├── requirements.txt
├── NetCHOS.png
├── examples
├── ma.xlsx
├── ufc.xlsx
├── README.rst
├── plot_heatmap.py
├── plot_network.py
└── plot_circular.py
├── MANIFEST.in
├── netchos
├── utils
│ ├── __init__.py
│ ├── categories.py
│ ├── misc.py
│ ├── colors.py
│ └── plot.py
├── io
│ ├── __init__.py
│ ├── io_mpl_to_px.json
│ ├── io_convert.py
│ ├── io_mpl_to_px.py
│ └── io_syslog.py
├── __init__.py
├── heatmap.py
├── network.py
└── circular.py
├── docs
├── source
│ ├── api.rst
│ ├── index.rst
│ └── conf.py
├── Makefile
└── make.bat
├── README.rst
├── LICENSE
├── .github
└── workflows
│ └── ci.yml
├── setup.py
└── .gitignore
/requirements.txt:
--------------------------------------------------------------------------------
1 | xarray
2 | plotly
3 | matplotlib
--------------------------------------------------------------------------------
/NetCHOS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brainets/netchos/HEAD/NetCHOS.png
--------------------------------------------------------------------------------
/examples/ma.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brainets/netchos/HEAD/examples/ma.xlsx
--------------------------------------------------------------------------------
/examples/ufc.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brainets/netchos/HEAD/examples/ufc.xlsx
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | # files that should came with installation
2 | include netchos/io/io_mpl_to_px.json
--------------------------------------------------------------------------------
/examples/README.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | ========
3 |
4 | Illustration of the main functions.
5 |
6 | .. contents:: Contents
7 | :local:
8 | :depth: 2
--------------------------------------------------------------------------------
/netchos/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .categories import categorize # noqa
2 | from .misc import normalize, norm_range, extract_df_cols # noqa
3 | from .plot import prepare_to_plot # noqa
4 |
--------------------------------------------------------------------------------
/netchos/io/__init__.py:
--------------------------------------------------------------------------------
1 | """I/O conversion functions."""
2 | from .io_convert import io_to_df # noqa
3 | from .io_mpl_to_px import mpl_to_px_inputs # noqa
4 | from .io_syslog import set_log_level, logger # noqa
--------------------------------------------------------------------------------
/netchos/io/io_mpl_to_px.json:
--------------------------------------------------------------------------------
1 | {
2 | "go.heatmap": {
3 | "cmap": "colorscale",
4 | "vmin": "zmin",
5 | "vmax": "zmax"
6 | },
7 | "line": {
8 | "lw": "width"
9 | }
10 | }
--------------------------------------------------------------------------------
/docs/source/api.rst:
--------------------------------------------------------------------------------
1 | API
2 | ===
3 |
4 | .. contents::
5 | :local:
6 | :depth: 2
7 |
8 | .. ----------------------------------------------------------------------------
9 |
10 | PLotting methods
11 | ----------------
12 |
13 | :py:mod:`netchos`:
14 |
15 | .. currentmodule:: netchos
16 |
17 | .. automodule:: netchos
18 | :no-members:
19 | :no-inherited-members:
20 |
21 | .. autosummary::
22 | :toctree: generated/
23 |
24 | heatmap
25 | network
26 | circular
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. NetCHOS documentation master file, created by
2 | sphinx-quickstart on Wed Apr 21 07:59:42 2021.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to NetCHOS's documentation!
7 | ===================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 |
14 |
15 | Content of the website
16 | ======================
17 |
18 | .. toctree::
19 | :maxdepth: 1
20 |
21 | auto_examples/index
22 | api
23 |
--------------------------------------------------------------------------------
/netchos/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Netchos
3 | =======
4 |
5 | Network, Connectivity and Hierarchically Organized Structures
6 | """
7 | import logging
8 |
9 | from netchos import io, utils # noqa
10 | from netchos.heatmap import heatmap # noqa
11 | from netchos.network import network # noqa
12 | from netchos.circular import circular # noqa
13 |
14 | __version__ = "0.0.0"
15 |
16 | # -----------------------------------------------------------------------------
17 | # Set 'info' as the default logging level
18 | logger = logging.getLogger('netchos')
19 | io.set_log_level('info')
20 |
--------------------------------------------------------------------------------
/netchos/utils/categories.py:
--------------------------------------------------------------------------------
1 | """Utility plotting functions to detect categories."""
2 | import numpy as np
3 |
4 | def categorize(x, cat):
5 | """Find categories bounds in a vector
6 |
7 | Parameters
8 | ----------
9 | x : array_like or list
10 | Array to convert
11 | cat : dict
12 | Dictionary in order to convert values of x
13 |
14 | Returns
15 | -------
16 | bounds : array_like
17 | Bounds where the category change
18 | """
19 | val = [cat[k] for k in x]
20 | _, u_int = np.unique(val, return_inverse=True)
21 | return np.where(np.diff(u_int) != 0)[0] + 1
22 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/netchos/io/io_convert.py:
--------------------------------------------------------------------------------
1 | """DataFrame, DataArray and NumPy conversions."""
2 | import numpy as np
3 | import pandas as pd
4 | import xarray as xr
5 |
6 | def io_to_df(x, xr_pivot=False):
7 | """Convert an input array to a DataFrame.
8 |
9 | Parameters
10 | ----------
11 | x : array_like or DataFrame or DataArray
12 | The object to convert
13 | xr_pivot : bool
14 | When using xarray.DataArray, use True for converting an already pivoted
15 | table or False for a table with categories in the columns
16 |
17 | Returns
18 | -------
19 | x_df : DataFrame
20 | Converted input to a DataFrame
21 | """
22 | if isinstance(x, xr.DataArray):
23 | if xr_pivot:
24 | x = x.to_pandas()
25 | else:
26 | x = x.to_dataframe('values').reset_index()
27 | if isinstance(x, np.ndarray):
28 | x = pd.DataFrame(x)
29 | assert isinstance(x, pd.DataFrame)
30 |
31 | return x
32 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =======
2 | NetCHOS
3 | =======
4 |
5 | NetCHOS = Network, Connectivity and Hierarchically Organized Structures
6 |
7 | Description
8 | -----------
9 |
10 | `NetCHOS `_ is a Python toolbox dedicated to network plotting, potentially with interactions, using standard plotting libraries (matplotlib, seaborn or plotly).
11 |
12 | .. image:: NetCHOS.png
13 |
14 | Documentation
15 | -------------
16 |
17 | Link to the documentation : https://brainets.github.io/netchos/
18 |
19 | Installation
20 | ------------
21 |
22 | Run the following command into your terminal to get the latest stable version :
23 |
24 |
25 | You can also install the latest version of the software directly from Github :
26 |
27 | .. code-block:: shell
28 |
29 | pip install git+https://github.com/brainets/netchos.git
30 |
31 |
32 | For developers, you can install it in develop mode with the following commands :
33 |
34 | .. code-block:: shell
35 |
36 | git clone https://github.com/brainets/netchos.git
37 | cd netchos
38 | python setup.py develop
39 | # or : pip install -e .
40 |
--------------------------------------------------------------------------------
/netchos/io/io_mpl_to_px.py:
--------------------------------------------------------------------------------
1 | """Conversion of Matplotlib / Seaborn inputs to plotly."""
2 | import os.path as op
3 | from pkg_resources import resource_filename
4 | import json
5 |
6 |
7 | def mpl_to_px_inputs(inputs, plt_types=None):
8 | """Convert typical matplotlib inputs to plotly to simplify API.
9 |
10 | Parameters
11 | ----------
12 | inputs : dict
13 | Dictionary of inputs
14 | plt_types : string or list or None
15 | Sub select some plotting types (e.g heatmap, line etc.). If None, all
16 | types are used
17 |
18 | Returns
19 | -------
20 | outputs : dict
21 | Dictionary of converted inputs
22 | """
23 | # load reference table
24 | file = op.join(op.dirname(__file__), "io_mpl_to_px.json")
25 | with open(file, 'r') as f:
26 | table = json.load(f)
27 |
28 | # go through the desired plotting types for conversion
29 | if plt_types is None:
30 | plt_types = list(table.keys())
31 | if isinstance(plt_types, str):
32 | plt_types = [plt_types]
33 | ref = {}
34 | for plt_type in plt_types:
35 | ref.update(table[plt_type])
36 |
37 | # convert inputs
38 | outputs = {}
39 | for k, v in inputs.items():
40 | if k in ref.keys():
41 | k = ref[k]
42 | outputs[k] = v
43 |
44 | return outputs
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2021, BraiNets
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/netchos/utils/misc.py:
--------------------------------------------------------------------------------
1 | """Miscellaneous functions."""
2 | import numpy as np
3 | import pandas as pd
4 |
5 |
6 | def normalize(x, to_min=0., to_max=1.):
7 | """Normalize the array x between to_min and to_max.
8 |
9 | Parameters
10 | ----------
11 | x : array_like
12 | The array to normalize
13 | to_min : int/float | 0.
14 | Minimum of returned array
15 | to_max : int/float | 1.
16 | Maximum of returned array
17 |
18 | Returns
19 | -------
20 | xn : array_like
21 | The normalized array
22 | """
23 | if to_min is None: to_min = np.nanmin(x) # noqa
24 | if to_max is None: to_max = np.nanmax(x) # noqa
25 | if x.size:
26 | xm, xh = np.nanmin(x), np.nanmax(x)
27 | if xm != xh:
28 | return to_max - (((to_max - to_min) * (xh - x)) / (xh - xm))
29 | else:
30 | return x * to_max / xh
31 | else:
32 | return x
33 |
34 |
35 | def norm_range(x, vmin=None, vmax=None, clip_min=0., clip_max=1.):
36 | if vmin is None: vmin = np.nanmin(x) # noqa
37 | if vmax is None: vmax = np.nanmax(x) # noqa
38 |
39 | if vmin < vmax:
40 | return np.clip((x - vmin) / (vmax - vmin), clip_min, clip_max)
41 | else:
42 | return np.full_like(x, 0.5)
43 |
44 |
45 | def extract_df_cols(data, **kwargs):
46 | """Extract DataFrame columns."""
47 | assert isinstance(data, pd.DataFrame)
48 | outs = {}
49 | for k, v in kwargs.items():
50 | if isinstance(v, str):
51 | outs[k] = data[v].values
52 | else:
53 | outs[k] = v
54 | return outs
55 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: "CI and Doc"
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 |
9 | test:
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | matrix:
13 | os: [macos-latest, windows-latest, ubuntu-16.04, ubuntu-18.04, ubuntu-20.04]
14 | python-version: [3.7, 3.8]
15 |
16 | steps:
17 | - name: Checkout 🛎️
18 | uses: actions/checkout@v2.3.1
19 |
20 | - name: Set up Python ${{ matrix.python-version }} 🔧
21 | uses: actions/setup-python@v2
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 |
25 | - name: Install dependencies 🔧
26 | run: |
27 | python -m pip install --upgrade pip
28 | pip install -e .
29 |
30 | deploy:
31 | needs: test
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout 🛎️
35 | uses: actions/checkout@v2.3.1
36 |
37 | - name: Set up Python 🔧
38 | uses: actions/setup-python@v2
39 | with:
40 | python-version: '3.7'
41 |
42 | - name: Install dependencies 🔧
43 | run: |
44 | python -m pip install --upgrade pip
45 | pip install -U sphinx sphinx-gallery kaleido sphinx_bootstrap_theme numpydoc xlrd==1.2.0
46 | pip install -e .
47 |
48 | - name: Build the Doc 🔧
49 | run: |
50 | cd docs
51 | make html
52 | touch build/html/.nojekyll
53 |
54 | - name: Deploy Github Pages 🚀
55 | uses: JamesIves/github-pages-deploy-action@4.1.1
56 | with:
57 | branch: gh-pages
58 | folder: docs/build/html/
59 | clean: true
60 | ssh-key: ${{ secrets.DEPLOY_KEY }}
61 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | # License: 3-clause BSD
4 | import os
5 | from setuptools import setup, find_packages
6 |
7 | __version__ = "0.0.0"
8 | NAME = 'netchos'
9 | AUTHOR = "BraiNets"
10 | MAINTAINER = "Etienne Combrisson"
11 | EMAIL = 'e.combrisson@gmail.com'
12 | KEYWORDS = "network connectivity plot matplotlib plotly"
13 | DESCRIPTION = "Network, Connectivity and Hierarchically Organized Structures"
14 | URL = 'https://github.com/brainets/netchos'
15 | DOWNLOAD_URL = ("https://github.com/brainets/netchos/archive/v" +
16 | __version__ + ".tar.gz")
17 | # Data path :
18 | PACKAGE_DATA = {}
19 |
20 |
21 | def read(fname):
22 | """Read README and LICENSE."""
23 | return open(os.path.join(os.path.dirname(__file__), fname)).read()
24 |
25 |
26 | with open('requirements.txt') as f:
27 | requirements = f.read().splitlines()
28 |
29 |
30 | setup(
31 | name=NAME,
32 | version=__version__,
33 | packages=find_packages(),
34 | package_dir={'netchos': 'netchos'},
35 | package_data=PACKAGE_DATA,
36 | include_package_data=True,
37 | description=DESCRIPTION,
38 | long_description=read('README.rst'),
39 | platforms='any',
40 | setup_requires=['numpy'],
41 | install_requires=requirements,
42 | dependency_links=[],
43 | author=AUTHOR,
44 | maintainer=MAINTAINER,
45 | author_email=EMAIL,
46 | url=URL,
47 | download_url=DOWNLOAD_URL,
48 | license="BSD 3-Clause License",
49 | keywords=KEYWORDS,
50 | classifiers=["Development Status :: 3 - Alpha",
51 | 'Intended Audience :: Science/Research',
52 | 'Intended Audience :: Education',
53 | 'Intended Audience :: Developers',
54 | 'Topic :: Scientific/Engineering :: Visualization',
55 | "Programming Language :: Python :: 3.6",
56 | "Programming Language :: Python :: 3.7",
57 | "Programming Language :: Python :: 3.8"
58 | ])
59 |
--------------------------------------------------------------------------------
/examples/plot_heatmap.py:
--------------------------------------------------------------------------------
1 | """
2 | Heatmap layout
3 | ==============
4 |
5 | Example illustrating the heatmap layout.
6 | """
7 | import os
8 |
9 | import numpy as np
10 | import pandas as pd
11 |
12 | from netchos import heatmap
13 |
14 | import plotly.io as pio
15 | import plotly.graph_objects as go
16 | from plotly.subplots import make_subplots
17 |
18 | pio.templates.default = 'plotly_white'
19 |
20 |
21 | ###############################################################################
22 | # Load the data
23 | # -------------
24 | #
25 |
26 | # load the connectivity 2D matrix
27 | ufc = pd.read_excel('ufc.xlsx', index_col=0)
28 | print(ufc)
29 |
30 | # load a table that contains informations about the nodes
31 | ma = pd.read_excel('ma.xlsx')
32 | print(ma)
33 |
34 | ###############################################################################
35 | # Default layout
36 | # --------------
37 |
38 | fig = heatmap(
39 | ufc
40 | )
41 | pio.show(fig)
42 |
43 | ###############################################################################
44 | # Adding categorical lines
45 | # ------------------------
46 |
47 | # # define manual boundaries
48 | vmin, vmax = 0., 0.02
49 |
50 | # create the categories (node_name: category_name)
51 | cat = {n: c for n, c in zip(ma['Name'], ma['Lobe'])}
52 |
53 | # sphinx_gallery_thumbnail_number = 2
54 | fig = heatmap(
55 | ufc, catline_x=cat, catline_y=cat, catline=dict(lw=2., color='white'),
56 | cmap='agsunset_r', vmin=vmin, vmax=vmax
57 | )
58 | fig.update_layout(title='Categorical lines', title_x=.5,
59 | template='plotly_dark')
60 | pio.show(fig)
61 |
62 | ###############################################################################
63 | # Heatmap layout in subplots
64 | # --------------------------
65 |
66 | fig = make_subplots(rows=1, cols=2, subplot_titles=('Subplot 1', 'Subplot 2'))
67 |
68 | # configuring the first subplot
69 | heatmap(
70 | ufc, catline_x=cat, catline_y=cat, catline=dict(lw=2., color='red'),
71 | vmin=vmin, vmax=vmax, cmap='plasma', fig=fig, kw_trace=dict(row=1, col=1)
72 | )
73 |
74 | # configuring the second subplot
75 | heatmap(
76 | ufc, catline_x=cat, catline_y=cat, catline=dict(lw=2., color='orange'),
77 | vmin=vmin, vmax=vmax, cmap='magma', fig=fig, kw_trace=dict(row=1, col=2)
78 | )
79 | fig.update_layout(width=1200, height=600)
80 | pio.show(fig)
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 | /generated
123 |
124 | # mypy
125 | .mypy_cache/
126 |
127 | # pycharm, vscode etc.
128 | .idea/
129 | .vscode/
130 |
131 | # files
132 | *.zip
133 | *.pdf
134 | *.prof
135 |
136 | /art
137 | docs/source/generated/
138 | docs/source/auto_examples/
139 | *.nc
140 | *.fuse*
141 |
142 | node_modules/
143 | package.json
144 | yarn.lock
--------------------------------------------------------------------------------
/netchos/heatmap.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 |
4 | from netchos.io import io_to_df, mpl_to_px_inputs
5 | from netchos.utils import categorize
6 |
7 |
8 | def heatmap(conn, catline_x=None, catline_y=None, catline=None,
9 | backend='plotly', kw_trace={}, fig=None, **kwargs):
10 | """Heatmap plot.
11 |
12 | Parameters
13 | ----------
14 | conn : array_like or DataFrame or DataArray
15 | 2D Connectivity matrix of shape (n_rows, n_cols). If conn is a
16 | DataFrame or a DataArray, the indexes and columns are used for the conn
17 | and y tick labels
18 | catline_x, catline_y : dict or None
19 | Dictionary in order to plot categorical lines along the conn and y
20 | axis. The keys should correspond to the index or column elements and
21 | the values to the category.
22 | catline : dict or None
23 | Additional arguments when plotting the line
24 | (e.g dict(color='red', lw=2))
25 | backend : {'mpl', 'plotly'}
26 | Backend to use for plotting. Use either 'mpl' for using matplotlib or
27 | 'plotly' for interactive figures using Plotly.
28 | fig : mpl.figure or go.Figure or None
29 | Figure object. Use either :
30 |
31 | * plt.figure when using matplotlib backend
32 | * plotly.graph_objects.Figure when using plotly backend
33 |
34 | kwargs : dict or {}
35 | Additional arguments are sent to :
36 |
37 | * seaborn.heatmap when using matplotlib backend
38 | * plotly.graph_objects.Heatmap when using plotly backend
39 |
40 | Returns
41 | -------
42 | fig : figure
43 | A matplotlib or plotly figure depending on the backend
44 | """
45 | if not isinstance(catline, dict):
46 | catline = {}
47 | catline['color'] = catline.get('color', 'white')
48 |
49 | # conn input conversion
50 | conn = io_to_df(conn, xr_pivot=True)
51 | index, columns = conn.index, conn.columns
52 |
53 | if backend == 'mpl': # -------------------------------- Matplotlib backend
54 | import seaborn as sns
55 | import matplotlib.pyplot as plt
56 | if fig is None:
57 | fig = plt.figure(figsize=(12, 9))
58 | kwargs['xticklabels'] = kwargs.get('xticklabels', True)
59 | kwargs['yticklabels'] = kwargs.get('yticklabels', True)
60 | # main heatmap
61 | ax = sns.heatmap(conn, **kwargs)
62 | # xlabel on top
63 | ax.xaxis.set_ticks_position('top')
64 | # categorical lines
65 | if isinstance(catline_x, dict):
66 | for k in categorize(columns, catline_x):
67 | ax.axvline(k, **catline)
68 | if isinstance(catline_y, dict):
69 | for k in categorize(index, catline_y):
70 | ax.axhline(k, **catline)
71 | elif backend == 'plotly': # ------------------------------- Plotly backend
72 | import plotly.graph_objects as go
73 | if fig is None:
74 | fig = go.Figure()
75 | # main heatmap
76 | kwargs = mpl_to_px_inputs(kwargs, "go.heatmap")
77 | catline = mpl_to_px_inputs(catline, "line")
78 | trace = go.Heatmap(z=conn, x=conn.columns, y=conn.index, **kwargs)
79 | fig.add_trace(trace, **kw_trace)
80 | fig.update_yaxes(tickmode='linear', autorange='reversed', **kw_trace)
81 | fig.update_xaxes(tickmode='linear', **kw_trace)
82 | if not len(kw_trace):
83 | fig.update_layout(width=900, height=850)
84 |
85 | # categorical lines
86 | if isinstance(catline_x, dict):
87 | for k in categorize(columns, catline_x):
88 | fig.add_vline(k - .5, line=catline)
89 | if isinstance(catline_y, dict):
90 | for k in categorize(index, catline_y):
91 | fig.add_hline(k - .5, line=catline)
92 |
93 | return fig
94 |
--------------------------------------------------------------------------------
/netchos/utils/colors.py:
--------------------------------------------------------------------------------
1 | """Color related functions."""
2 | import numpy as np
3 |
4 |
5 |
6 | def get_colorscale_values(cmap):
7 | """Get the colors composing a plotly colorscale.
8 |
9 | Parameter
10 | ---------
11 | cmap : str
12 | Name of the Plotly colorscale
13 |
14 | Returns
15 | -------
16 | colorscale : array_like
17 | Colors associated to the colormap
18 | """
19 | import plotly
20 |
21 | rev = '_r' if '_r' in cmap.lower() else ''
22 | cmap = cmap.lower().replace('_r', '')
23 | colorscales = plotly.colors.named_colorscales()
24 | assert cmap in colorscales
25 | ensembles = ['sequential', 'diverging', 'qualitative']
26 | for e in ensembles:
27 | cmaps = dir(eval(f'plotly.colors.{e}'))
28 | cmaps_lower = [c.lower() for c in cmaps]
29 | if cmap in cmaps_lower:
30 | cmap_idx = cmaps_lower.index(cmap)
31 | return eval(f'plotly.colors.{e}.{cmaps[cmap_idx]}{rev}')
32 | assert ValueError(f"{cmap} is not a predefined colorscale {colorscales}")
33 |
34 |
35 | def hex_to_rgb(value):
36 | """Convert a hex-formatted color to rgb, ignoring alpha values."""
37 | value = value.lstrip("#")
38 | return [int(value[i:i + 2], 16) for i in range(0, 6, 2)]
39 |
40 |
41 | def rbg_to_hex(c):
42 | """Convert an rgb-formatted color to hex, ignoring alpha values."""
43 | return f"#{c[0]:02x}{c[1]:02x}{c[2]:02x}"
44 |
45 |
46 | def plotly_map_color(vals, colorscale, vmin=None, vmax=None, return_hex=True):
47 | """Given a float array vals, interpolate based on a colorscale to obtain
48 | rgb or hex colors. Inspired by
49 | `user empet's answer in \
50 | `_."""
51 | from numbers import Number
52 | from ast import literal_eval
53 |
54 | if vmin is None: vmin = np.nanmin(vals) # noqa
55 | if vmax is None: vmax = np.nanmax(vals) # noqa
56 |
57 | if vmin > vmax:
58 | raise ValueError("`vmin` should be <= `vmax`.")
59 |
60 | if isinstance(colorscale, str):
61 | colorscale = get_colorscale_values(colorscale)
62 |
63 | if (len(colorscale[0]) == 2) and isinstance(colorscale[0][0], Number):
64 | scale, colors = zip(*colorscale)
65 | else:
66 | scale = np.linspace(0, 1, num=len(colorscale))
67 | colors = colorscale
68 | scale = np.asarray(scale)
69 |
70 | if colors[0][:3] == "rgb":
71 | colors = np.asarray([literal_eval(color[3:]) for color in colors],
72 | dtype=np.float_)
73 | elif colors[0][0] == "#":
74 | colors = np.asarray(list(map(hex_to_rgb, colors)), dtype=np.float_)
75 | else:
76 | raise ValueError("This colorscale is not supported.")
77 |
78 | colorscale = np.hstack([scale.reshape(-1, 1), colors])
79 | colorscale = np.vstack([colorscale, colorscale[0, :]])
80 | colorscale_diffs = np.diff(colorscale, axis=0)
81 | colorscale_diff_ratios = colorscale_diffs[:, 1:] / colorscale_diffs[:, [0]]
82 | colorscale_diff_ratios[-1, :] = np.zeros(3)
83 |
84 | if vmin < vmax:
85 | vals_scaled = (vals - vmin) / (vmax - vmin)
86 | else:
87 | vals_scaled = np.full(vals.shape, 0.5)
88 |
89 | left_bin_indices = np.digitize(vals_scaled, scale) - 1
90 | left_endpts = colorscale[left_bin_indices]
91 | vals_scaled -= left_endpts[:, 0]
92 | diff_ratios = colorscale_diff_ratios[left_bin_indices]
93 |
94 | vals_rgb = (
95 | left_endpts[:, 1:] + diff_ratios * vals_scaled[:, np.newaxis] + 0.5
96 | ).astype(np.uint8)
97 |
98 | if return_hex:
99 | return list(map(rbg_to_hex, vals_rgb))
100 | return [f"rgb{tuple(v)}" for v in vals_rgb]
101 |
102 | if __name__ == '__main__':
103 | import matplotlib.pyplot as plt
104 | # print(dir(plt.get_cmap('viridis')))
105 | print(plt.get_cmap('viridis').colors)
106 | exit()
107 | import plotly
108 | colors = plotly_map_color(np.arange(10), 'thermal_r')
109 | print(colors)
--------------------------------------------------------------------------------
/examples/plot_network.py:
--------------------------------------------------------------------------------
1 | """
2 | Network layout
3 | ==============
4 |
5 | Example illustrating the network layout
6 | """
7 | import os
8 |
9 | import numpy as np
10 | import pandas as pd
11 |
12 | from netchos import network
13 |
14 | import plotly.io as pio
15 | import plotly.graph_objects as go
16 | from plotly.subplots import make_subplots
17 |
18 | pio.templates.default = 'plotly_white'
19 |
20 |
21 | ###############################################################################
22 | # Load the data
23 | # -------------
24 | #
25 |
26 | # load the connectivity 2D matrix
27 | ufc = pd.read_excel('ufc.xlsx', index_col=0)
28 | print(ufc)
29 |
30 | # load a table that contains informations about the nodes
31 | ma = pd.read_excel('ma.xlsx')
32 | print(ma)
33 |
34 | # computes nodes' degree
35 | ma['degree'] = (~np.isnan(ufc.values)).sum(0)
36 | # compute node's strength
37 | ma['strength'] = np.nansum(ufc, axis=0)
38 |
39 |
40 | ###############################################################################
41 | # Default 2D layout
42 | # -----------------
43 |
44 | fig = network(
45 | ufc, # 2D connectivity matrix
46 | nodes_data=ma, # dataframe with data attached to each node
47 | nodes_x='xcoord_2D', # x-coordinate name in nodes_data table
48 | nodes_y='ycoord_2D' # y-coordinate name in nodes_data table
49 | )
50 | pio.show(fig)
51 |
52 | ###############################################################################
53 | # Default 3D layout
54 | # -----------------
55 |
56 | fig = network(
57 | ufc, # 2D connectivity matrix
58 | nodes_data=ma, # dataframe with data attached to each node
59 | nodes_x='xcoord_3D', # x-coordinate name in nodes_data table
60 | nodes_y='ycoord_3D', # y-coordinate name in nodes_data table
61 | nodes_z='zcoord_3D' # z-coordinate name in nodes_data table
62 | )
63 | pio.show(fig)
64 |
65 | ###############################################################################
66 | # Control of aesthetics
67 | # ---------------------
68 |
69 | # sphinx_gallery_thumbnail_number = 3
70 | fig = network(
71 | ufc, nodes_data=ma,
72 | nodes_x='xcoord_2D', # x-coordinate (column name in ma)
73 | nodes_y='ycoord_2D', # y-coordinate (column name in ma)
74 | nodes_color='degree', # color of the node given by the degree
75 | nodes_size='strength', # marker size proportional to the strentgh
76 | nodes_size_min=1., # minimum size of the nodes
77 | nodes_size_max=30., # maximum size of the nodes
78 | nodes_cmap='agsunset_r', # colormap associated to the nodes
79 | edges_cmap='agsunset_r', # colormap associated to the edges
80 | edges_opacity_min=.5, # weak connections semi-transparents
81 | edges_opacity_max=1., # strong connections opaques
82 | cbar_title='UFC'
83 | )
84 |
85 | title = 'Control of aesthetics'
86 | fig.update_layout(template='plotly_dark', title=title, title_x=.5)
87 | pio.show(fig)
88 |
89 | ###############################################################################
90 | # Network layout in subplots
91 | # --------------------------
92 |
93 | fig = make_subplots(rows=1, cols=2, subplot_titles=('Subplot 1', 'Subplot 2'))
94 |
95 | # configuring the first subplot
96 | network(
97 | ufc, nodes_data=ma, nodes_x='xcoord_2D', nodes_y='ycoord_2D',
98 | nodes_name='Name', nodes_size='degree', nodes_color='degree',
99 | nodes_cmap='plasma_r', edges_cmap='plasma_r', fig=fig,
100 | edges_opacity_min=0., edges_opacity_max=1., kw_trace=dict(row=1, col=1),
101 | kw_cbar=dict(x=0.45)
102 | )
103 |
104 | # configuring the second subplot
105 | network(
106 | ufc, nodes_data=ma, nodes_x='xcoord_2D', nodes_y='ycoord_2D',
107 | nodes_name='Name', nodes_size='strength', nodes_color='strength',
108 | nodes_cmap='magma_r', edges_cmap='magma_r', fig=fig,
109 | edges_opacity_min=.6, edges_opacity_max=.8, kw_trace=dict(row=1, col=2),
110 | kw_cbar=dict(x=1.)
111 | )
112 |
113 | title = "Illustration of adding network layouts to subplots"
114 | fig.update_layout(width=1200, height=600, title=title, title_x=0.5)
115 | pio.show(fig)
--------------------------------------------------------------------------------
/examples/plot_circular.py:
--------------------------------------------------------------------------------
1 | """
2 | Circular layout
3 | ===============
4 |
5 | Example illustrating the circular layout
6 | """
7 | import os
8 |
9 | import numpy as np
10 | import pandas as pd
11 |
12 | from netchos import circular
13 |
14 | import plotly.io as pio
15 | import plotly.graph_objects as go
16 | from plotly.subplots import make_subplots
17 |
18 | pio.templates.default = 'plotly_white'
19 |
20 |
21 | ###############################################################################
22 | # Load the data
23 | # -------------
24 | #
25 |
26 | # load the connectivity 2D matrix
27 | ufc = pd.read_excel('ufc.xlsx', index_col=0)
28 | print(ufc)
29 |
30 | # load a table that contains informations about the nodes
31 | ma = pd.read_excel('ma.xlsx')
32 | print(ma)
33 |
34 | # computes nodes' degree
35 | ma['degree'] = (~np.isnan(ufc.values)).sum(0)
36 | # compute node's strength
37 | ma['strength'] = np.nansum(ufc, axis=0)
38 |
39 | ###############################################################################
40 | # Default layout
41 | # --------------
42 |
43 | title = "Default layout (nodes degree represented by marker size and color)"
44 | fig = circular(
45 | ufc
46 | )
47 | fig.update_layout(title=title)
48 | pio.show(fig)
49 |
50 | ###############################################################################
51 | # Passing data to the nodes
52 | # -------------------------
53 |
54 | """
55 | Here, we use the `nodes_data` input to provide a pandas DataFrame containing
56 | nodes informations, namely :
57 | - The name of the nodes ('Name')
58 | - The number of connections per node ('degree')
59 | - The connectivity strength ('strength')
60 | - Additional categorization (each node is a brain region that belong to a lobe)
61 | """
62 | fig = circular(
63 | ufc, nodes_data=ma, nodes_name='Name', nodes_size='degree',
64 | nodes_color='strength', categories='Lobe'
65 | )
66 | pio.show(fig)
67 |
68 | ###############################################################################
69 | # Control of aesthetics
70 | # ---------------------
71 |
72 | kw_circ = dict()
73 |
74 | # nodes settings
75 | kw_circ['nodes_size_min'] = 0.2
76 | kw_circ['nodes_size_max'] = 10
77 | kw_circ['nodes_cmap'] = 'plasma' # colormap for the nodes
78 |
79 | # edges settings
80 | kw_circ['edges_width_min'] = .5
81 | kw_circ['edges_width_max'] = 9.
82 | kw_circ['edges_opacity_min'] = 0.2 # opacity for weakest connections
83 | kw_circ['edges_opacity_max'] = 1. # opacity for strongest connections
84 | kw_circ['edges_cmap'] = 'agsunset' # colormap for the edges
85 |
86 | # layout settings
87 | kw_circ['angle_start'] = 90 # start circle at 90°
88 | kw_circ['angle_range'] = 180 # use only half of the circle
89 | kw_circ['cbar_title'] = 'Significant links (p<0.05)'
90 |
91 | # sphinx_gallery_thumbnail_number = 3
92 | fig = circular(
93 | ufc, nodes_data=ma, nodes_name='Name', nodes_size='degree',
94 | nodes_color='strength', categories='Lobe', **kw_circ
95 | )
96 | fig.update_layout(width=600, height=700, title='Control of aesthetics',
97 | title_x=0.5, template='plotly_dark')
98 | pio.show(fig)
99 |
100 | ###############################################################################
101 | # Circular layout in subplots
102 | # ---------------------------
103 |
104 | fig = make_subplots(rows=1, cols=2, subplot_titles=('Subplot 1', 'Subplot 2'))
105 |
106 | # configuring the first subplot
107 | circular(
108 | ufc, nodes_data=ma, nodes_name='Name', nodes_size='degree',
109 | nodes_color='degree', categories='Lobe', nodes_cmap='plasma_r',
110 | edges_cmap='plasma_r', angle_range=180, fig=fig,
111 | kw_trace=dict(row=1, col=1), kw_cbar=dict(x=0.4)
112 | )
113 |
114 | # configuring the second subplot
115 | circular(
116 | ufc, nodes_data=ma, nodes_name='Name', nodes_size='strength',
117 | nodes_color='strength', categories='Lobe', nodes_cmap='magma_r',
118 | edges_cmap='magma_r', angle_range=180, fig=fig,
119 | kw_trace=dict(row=1, col=2), kw_cbar=dict(x=0.95)
120 | )
121 |
122 | title = "Illustration of adding circular layouts to subplots"
123 | fig.update_layout(width=1000, height=800, title=title, title_x=0.5)
124 | pio.show(fig)
--------------------------------------------------------------------------------
/netchos/io/io_syslog.py:
--------------------------------------------------------------------------------
1 | """Netchos logger.
2 |
3 | See :
4 | https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output
5 | """
6 | import logging
7 | import sys
8 | import re
9 |
10 |
11 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
12 | RESET_SEQ = "\033[0m"
13 | COLOR_SEQ = "\033[1;%dm"
14 | BOLD_SEQ = "\033[1m"
15 | COLORS = {
16 | 'DEBUG': GREEN,
17 | 'PROFILER': MAGENTA,
18 | 'INFO': WHITE,
19 | 'WARNING': YELLOW,
20 | 'ERROR': RED,
21 | 'CRITICAL': RED,
22 | }
23 | FORMAT = {'compact': "$BOLD%(levelname)s | %(message)s",
24 | 'spacy': "$BOLD%(levelname)-19s$RESET | %(message)s",
25 | 'netchos': "$BOLD%(name)s-%(levelname)-19s$RESET | %(message)s",
26 | 'print': "%(message)s",
27 | }
28 |
29 |
30 | def formatter_message(message, use_color=True):
31 | """Format the message."""
32 | return message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
33 |
34 |
35 | class _Formatter(logging.Formatter):
36 | """Formatter."""
37 |
38 | def __init__(self, format_type='compact'):
39 | logging.Formatter.__init__(self, FORMAT[format_type])
40 | self._format_type = format_type
41 |
42 | def format(self, record):
43 | name = record.levelname
44 | msg = record.getMessage()
45 | # If * in msg, set it in RED :
46 | if '*' in msg:
47 | regexp = '\*.*?\*'
48 | re_search = re.search(regexp, msg).group()
49 | to_color = COLOR_SEQ % (30 + RED) + re_search + COLOR_SEQ % (
50 | 30 + WHITE) + RESET_SEQ
51 | msg_color = re.sub(regexp, to_color, msg)
52 | msg_color += RESET_SEQ
53 | record.msg = msg_color
54 | # Set level color :
55 | levelname_color = COLOR_SEQ % (30 + COLORS[name]) + name + RESET_SEQ
56 | record.levelname = levelname_color
57 | if record.levelno == 20:
58 | logging.Formatter.__init__(self, FORMAT['print'])
59 | else:
60 | logging.Formatter.__init__(self, FORMAT[self._format_type])
61 | return formatter_message(logging.Formatter.format(self, record))
62 |
63 |
64 | class _StreamHandler(logging.StreamHandler):
65 | """Stream handler allowing matching and recording."""
66 |
67 | def __init__(self):
68 | logging.StreamHandler.__init__(self, sys.stderr)
69 | self.setFormatter(_lf)
70 | self._str_pattern = None
71 | self.emit = self._netchos_emit
72 |
73 | def _netchos_emit(self, record, *args):
74 | msg = record.getMessage()
75 | test = self._match_pattern(record, msg)
76 | if test:
77 | record.msg = test
78 | return logging.StreamHandler.emit(self, record)
79 | else:
80 | return
81 |
82 | def _match_pattern(self, record, message):
83 | if isinstance(self._str_pattern, str):
84 | if re.search(self._str_pattern, message):
85 | sub = '*{}*'.format(self._str_pattern)
86 | return re.sub(self._str_pattern, sub, message)
87 | else:
88 | return ''
89 | else:
90 | return message
91 |
92 |
93 | logger = logging.getLogger('netchos')
94 | # logger.propagate = True
95 | _lf = _Formatter()
96 | _lh = _StreamHandler() # needs _lf to exist
97 | logger.addHandler(_lh)
98 | PROFILER_LEVEL_NUM = 1
99 | logging.addLevelName(PROFILER_LEVEL_NUM, "PROFILER")
100 |
101 |
102 | def profiler_fcn(self, message, *args, **kws): # noqa
103 | # Yes, logger takes its '*args' as 'args'.
104 | if self.isEnabledFor(PROFILER_LEVEL_NUM):
105 | self._log(PROFILER_LEVEL_NUM, message, args, **kws)
106 |
107 |
108 | logging.Logger.profiler = profiler_fcn
109 | LOGGING_TYPES = dict(DEBUG=logging.DEBUG, INFO=logging.INFO,
110 | WARNING=logging.WARNING, ERROR=logging.ERROR,
111 | CRITICAL=logging.CRITICAL, PROFILER=PROFILER_LEVEL_NUM)
112 |
113 |
114 | def set_log_level(verbose=None, match=None):
115 | """Convenience function for setting the logging level.
116 |
117 | This function comes from the PySurfer package. See :
118 | https://github.com/nipy/PySurfer/blob/master/surfer/utils.py
119 |
120 | Parameters
121 | ----------
122 | verbose : bool, str, int, or None
123 | The verbosity of messages to print. If a str, it can be either
124 | PROFILER, DEBUG, INFO, WARNING, ERROR, or CRITICAL.
125 | match : string | None
126 | Filter logs using a string pattern.
127 | """
128 | # if verbose is None:
129 | # verbose = "INFO"
130 | logger = logging.getLogger('netchos')
131 | if isinstance(verbose, bool):
132 | verbose = 'INFO' if verbose else 'WARNING'
133 | if verbose is None:
134 | verbose = 'INFO'
135 | if isinstance(verbose, str):
136 | if (verbose.upper() in LOGGING_TYPES):
137 | verbose = verbose.upper()
138 | verbose = LOGGING_TYPES[verbose]
139 | logger.setLevel(verbose)
140 | else:
141 | raise ValueError("verbose must be in "
142 | "%s" % ', '.join(LOGGING_TYPES))
143 | if isinstance(match, str):
144 | _lh._str_pattern = match
145 |
146 |
147 | class use_log_level(object): # noqa
148 | """Context handler for logging level.
149 |
150 | Parameters
151 | ----------
152 | level : int
153 | The level to use.
154 | """
155 |
156 | def __init__(self, level): # noqa
157 | self.level = level
158 |
159 | def __enter__(self): # noqa
160 | self.old_level = set_log_level(self.level, True)
161 |
162 | def __exit__(self, *args): # noqa
163 | set_log_level(self.old_level)
164 |
165 |
166 | def progress_bar(value, endvalue, bar_length=20, pre_st=None):
167 | """Progress bar."""
168 | percent = float(value) / endvalue
169 | arrow = '-' * int(round(percent * bar_length) - 1) + '>'
170 | spaces = ' ' * (bar_length - len(arrow))
171 | pre_st = '' if not isinstance(pre_st, str) else pre_st
172 |
173 | sys.stdout.write("\r{0} [{1}] {2}%".format(pre_st, arrow + spaces,
174 | int(round(percent * 100))))
175 | sys.stdout.flush()
176 |
--------------------------------------------------------------------------------
/netchos/utils/plot.py:
--------------------------------------------------------------------------------
1 | """Plotting utility function."""
2 | import pandas as pd
3 | import numpy as np
4 |
5 | from collections import OrderedDict
6 |
7 | from .misc import normalize, norm_range
8 |
9 | def prepare_to_plot(
10 | conn, nodes_name=None, nodes_size=None, nodes_size_min=1,
11 | nodes_size_max=10, nodes_color=None, nodes_x=None, nodes_y=None,
12 | nodes_z=None, nodes_text=None, edges_min=None, edges_max=None,
13 | edges_width_min=.5, edges_width_max=8, edges_opacity_min=.1,
14 | edges_opacity_max=1., edges_cmap='plasma', edges_sorted=True,
15 | edges_rm_missing=True, directed=False, backend='plotly'):
16 | """Function to extract variables that are then used for plotting graph.
17 |
18 | Parameters
19 | ----------
20 | conn : pd.DataFrame
21 | 2D connectivity array
22 | node_names : list | None
23 | List of node names. If None, the index of the dataframe are used
24 | nodes_size : list | None
25 | List of values to use in order to modulate marker's sizes. If None, the
26 | density of the graph is used in place
27 | node_size_min, node_size_max : float | 1, 10
28 | Respectively, the minimum and maximum size to use for the markers
29 | nodes_color : list | None
30 | List of values to use in order to modulate marker's color. If None, the
31 | density of the graph is used in place
32 | nodes_x, nodes_y, nodes_z : list | None
33 | List of values to use respectively for the x, y and z coordinates
34 | edges_min, edges_max : float | None
35 | Respectively the minimum and maximum to use for clipping edges values
36 | edges_width_min, edges_width_max : float | .5, .8
37 | Respectively the minimum and maximum width to use fot the edges
38 | edges_opacity_min, edges_opacity_max : float | .1, 1.
39 | Respectively the minimum and maximum opacity to use for the edges
40 | edges_sorted : bool | True
41 | Specify whether the edges should be sorted according to their values
42 | edges_rm_missing : bool | True
43 | Specify whether missing connections should be removed
44 | directed : bool | False
45 | Specify if the graph is undirected (False) or directed (True)
46 |
47 | Returns
48 | -------
49 | df_nodes : pd.DataFrame
50 | Pandas DataFrame containing relevant informations about nodes
51 | df_edges : pd.DataFrame
52 | Pandas DataFrame containing relevant informations about edges
53 | """
54 | # -------------------------------------------------------------------------
55 | # NODES DATAFRAME
56 | # -------------------------------------------------------------------------
57 | n_nodes = conn.shape[0]
58 | df_nodes = OrderedDict()
59 |
60 | # nodes names
61 | if nodes_name is None:
62 | if isinstance(conn, pd.DataFrame):
63 | nodes_name = [str(k) for k in conn.index]
64 | else:
65 | nodes_name = [str(k) for k in range(n_nodes)]
66 | df_nodes['name'] = nodes_name
67 |
68 | # compute node degree
69 | if directed:
70 | raise NotImplementedError("degree of directed graph")
71 | else:
72 | df_nodes['degree'] = (~np.isnan(conn)).sum(axis=0).astype(int)
73 |
74 | # nodes marker size (default=degree)
75 | if nodes_size is None:
76 | df_nodes['size'] = df_nodes['degree']
77 | else:
78 | df_nodes['size'] = nodes_size
79 | df_nodes['size_plt'] = normalize(
80 | df_nodes['size'], nodes_size_min, nodes_size_max)
81 |
82 | # nodes marker color (default=degree)
83 | if nodes_color is None:
84 | df_nodes['color'] = df_nodes['degree']
85 | else:
86 | df_nodes['color'] = nodes_color
87 | df_nodes['color_plt'] = normalize(df_nodes['color'], 0., 1.)
88 |
89 | # nodes coordinates
90 | if nodes_x is None:
91 | nodes_x = np.full((n_nodes,), np.nan)
92 | if nodes_y is None:
93 | nodes_y = np.full((n_nodes,), np.nan)
94 | if nodes_z is None:
95 | nodes_z = np.full((n_nodes,), np.nan)
96 | df_nodes['x'], df_nodes['y'], df_nodes['z'] = nodes_x, nodes_y, nodes_z
97 |
98 | # nodes text
99 | if nodes_text is None:
100 | nodes_text = {n: n for n in list(df_nodes['name'])}
101 | df_nodes['text'] = [nodes_text[n] for n in list(df_nodes['name'])]
102 |
103 | # dataframe conversion
104 | df_nodes = pd.DataFrame(df_nodes)
105 |
106 | # -------------------------------------------------------------------------
107 | # EDGES DATAFRAME
108 | # -------------------------------------------------------------------------
109 | df_edges = OrderedDict()
110 |
111 | # get triangle indices
112 | if directed:
113 | raise NotImplementedError("Not implemented for directed graph")
114 | else:
115 | tri_s, tri_t = np.triu_indices_from(conn, k=1)
116 |
117 | # if required, drop edges with nan values
118 | if edges_rm_missing:
119 | _is_nan = ~np.isnan(np.array(conn)[tri_s, tri_t])
120 | tri_s, tri_t = tri_s[_is_nan], tri_t[_is_nan]
121 | df_edges['s'], df_edges['t'] = tri_s, tri_t
122 |
123 | # edges names
124 | sep = '->' if directed else '-'
125 | s_names, t_names = df_nodes['name'][tri_s], df_nodes['name'][tri_t]
126 | df_edges['names'] = [f"{s}{sep}{t}" for s, t in zip(s_names, t_names)]
127 |
128 | # edges values
129 | edges_val = np.array(conn)[tri_s, tri_t]
130 | df_edges['values'] = edges_val
131 | df_edges['colorbar'] = normalize(
132 | edges_val, to_min=edges_min, to_max=edges_max)
133 |
134 | # plotting edges values
135 | values = norm_range(edges_val, vmin=edges_min, vmax=edges_max)
136 | df_edges['values_plt'] = values
137 | df_edges['order'] = np.argsort(values)
138 |
139 | # plotting edges width
140 | df_edges['width'] = (values * (edges_width_max - edges_width_min)) + \
141 | edges_width_min
142 |
143 | # plotting edge opacity
144 | df_edges['opacity'] = (values * (edges_opacity_max - \
145 | edges_opacity_min)) + edges_opacity_min
146 |
147 | # plotting color
148 | if backend == 'mpl':
149 | from matplotlib.colors import to_hex
150 | import matplotlib.pyplot as plt
151 |
152 | cmap = plt.get_cmap(edges_cmap)
153 | df_edges['color'] = [to_hex(cmap(k)) for k in df_edges['values_plt']]
154 | elif backend == 'plotly':
155 | from netchos.utils.colors import plotly_map_color
156 |
157 | df_edges['color'] = plotly_map_color(
158 | df_edges['values'], edges_cmap, vmin=edges_min, vmax=edges_max)
159 |
160 | # edges coordinates
161 | df_edges['x_s'], df_edges['x_t'] = nodes_x[tri_s], nodes_x[tri_t]
162 | df_edges['y_s'], df_edges['y_t'] = nodes_y[tri_s], nodes_y[tri_t]
163 | df_edges['z_s'], df_edges['z_t'] = nodes_z[tri_s], nodes_z[tri_t]
164 |
165 | # dataframe conversion
166 | df_edges = pd.DataFrame(df_edges)
167 |
168 |
169 | if edges_sorted:
170 | df_edges = df_edges.loc[
171 | df_edges['order'].values].reset_index(drop=True)
172 |
173 | return df_nodes, df_edges
174 |
--------------------------------------------------------------------------------
/netchos/network.py:
--------------------------------------------------------------------------------
1 | """Network plotting layout."""
2 | import pandas as pd
3 | import numpy as np
4 |
5 | from netchos.io import io_to_df
6 | from netchos.utils import normalize, extract_df_cols, prepare_to_plot
7 |
8 |
9 | def network(
10 | conn, nodes_data=None, nodes_name=None, nodes_x=None, nodes_y=None,
11 | nodes_z=None, nodes_color=None, nodes_size=None, nodes_size_min=1,
12 | nodes_size_max=30, nodes_cmap='plasma', nodes_text=None, edges_min=None,
13 | edges_max=None, edges_width_min=.5, edges_width_max=8,
14 | edges_opacity_min=0.1, edges_opacity_max=1., edges_cmap='plasma',
15 | cbar=True, cbar_title='Edges', directed=False, fig=None, kw_trace={},
16 | kw_cbar={}):
17 | """Network plotting, either in 2D or 3D.
18 |
19 | Parameters
20 | ----------
21 | conn : array_like or DataFrame or DataArray
22 | 2D Connectivity matrix of shape (n_rows, n_cols). If conn is a
23 | DataFrame or a DataArray, the indexes and columns are used for the conn
24 | and y tick labels
25 | nodes_data : pd.DataFrame
26 | DataFrame that can contains nodes informations (e.g the name of the
27 | nodes, the x, y and z coordinates, values assign to the nodes etc.)
28 | nodes_name : list, array_like, str | None
29 | List of names of the nodes. Alternatively, if `nodes_data` is provided,
30 | a string referring to a column name can be provided instead
31 | nodes_x, nodes_y, nodes_z : list, array_like, str | None
32 | The x, y and potentially z coordinates of each node. Alternatively, if
33 | `nodes_data` is provided, a string referring to a column name can be
34 | provided instead
35 | nodes_color, nodes_size : list, array_like, str | None
36 | List of values assign to each node in order to modulate respectively
37 | the color and the size of the nodes. If None, the degree of each node
38 | is going to be used instead. Alternatively, if `nodes_data` is
39 | provided, a string referring to a column name can be provided instead
40 | nodes_size_min, nodes_size_max : float | 1, 30
41 | Respectively, the minimum and maximum size to use for the markers
42 | nodes_cmap : str | 'plasma'
43 | Colormap to use in order to infer the color of each node
44 | edges_min, edges_max : float | None
45 | Respectively the minimum and maximum to use for clipping edges values
46 | edges_width_min, edges_width_max : float | .5, .8
47 | Respectively the minimum and maximum width to use fot the edges
48 | edges_opacity_min, edges_opacity_max : float | 0.1, 1.
49 | Respectively the minimum and maximum opacity for edges
50 | edges_cmap : str | 'plasma'
51 | Colormap to use to infer the color of each edge
52 | cbar : bool | True
53 | Add a colorbar to the plot.
54 | cbar_title : str | 'Edges
55 | Default colorbar title
56 | directed : bool | False
57 | Specify if the graph is undirected (False) or directed (True)
58 | fig : go.Figure | None
59 | plotly.graph_objects.Figure object
60 | kw_trace : dict | {}
61 | Additional arguments to pass to the
62 | plotly.graph_objects.Figure.add_trace method
63 |
64 | Returns
65 | -------
66 | fig : go.Figure | None
67 | A plotly.graph_objects.Figure object containing either the 2D network
68 | or a 3D network if the z coordinate is provided
69 | """
70 | import plotly.graph_objects as go
71 |
72 | # -------------------------------------------------------------------------
73 | # I/O
74 | # -------------------------------------------------------------------------
75 | # connectivity matrix conversion
76 | conn = io_to_df(conn, xr_pivot=True)
77 | plt_in = '3D' if nodes_z is not None else '2D'
78 |
79 | # get node names and coordinates in case of dataframe
80 | kw_nodes = dict(nodes_name=nodes_name, nodes_size=nodes_size,
81 | nodes_x=nodes_x, nodes_y=nodes_y, nodes_z=nodes_z,
82 | nodes_color=nodes_color)
83 | if isinstance(nodes_data, pd.DataFrame):
84 | kw_nodes = extract_df_cols(nodes_data, **kw_nodes)
85 |
86 | # get useful variables for plotting
87 | df_nodes, df_edges = prepare_to_plot(
88 | conn, nodes_size_min=nodes_size_min, nodes_size_max=nodes_size_max,
89 | nodes_text=nodes_text, edges_min=edges_min, edges_max=edges_max,
90 | edges_width_min=edges_width_min, edges_width_max=edges_width_max,
91 | edges_opacity_min=edges_opacity_min,
92 | edges_opacity_max=edges_opacity_max, directed=directed,
93 | edges_cmap=edges_cmap, edges_sorted=True, edges_rm_missing=True,
94 | **kw_nodes
95 | )
96 |
97 | # -------------------------------------------------------------------------
98 | # PLOT VARIABLES
99 | # -------------------------------------------------------------------------
100 | # build edges lines
101 | edges_x = np.c_[df_edges['x_s'], df_edges['x_t']]
102 | edges_y = np.c_[df_edges['y_s'], df_edges['y_t']]
103 | edges_z = np.c_[df_edges['z_s'], df_edges['z_t']]
104 |
105 | # automatic nodes_size ratio
106 | sizeref = np.max(df_nodes['size_plt']) / nodes_size_max ** 2
107 |
108 | # prepare hover data
109 | hovertemplate = (
110 | "Node : %{text}
Size : %{customdata[0]:.3f}
"
111 | "Color : %{customdata[1]:.3f}
"
112 | "Degree : %{customdata[2]}")
113 |
114 | # hover custom data
115 | customdata = np.stack(
116 | (df_nodes['size'], df_nodes['color'], df_nodes['degree']), axis=-1)
117 |
118 | if fig is None:
119 | fig = go.Figure()
120 |
121 | # switch between 2D and 3D representations
122 | Scatter = go.Scatter3d if plt_in == '3D' else go.Scatter
123 |
124 | # -------------------------------------------------------------------------
125 | # NODES PLOT
126 | # -------------------------------------------------------------------------
127 | # node plot
128 | kw_nodes = dict(x=list(df_nodes['x']), y=list(df_nodes['y']))
129 | if plt_in == '3D':
130 | kw_nodes['z'] = list(df_nodes['z'])
131 | node_trace = Scatter(
132 | mode='markers+text', text=list(df_nodes['text']), name='Nodes',
133 | textposition="top center", hovertemplate=hovertemplate,
134 | customdata=customdata, marker=dict(
135 | showscale=False, colorscale=nodes_cmap, sizemode='area',
136 | sizeref=sizeref, opacity=1., size=list(df_nodes['size_plt']),
137 | color=list(df_nodes['color_plt']),
138 | ), **kw_nodes
139 | )
140 |
141 | # -------------------------------------------------------------------------
142 | # EDGES PLOT
143 | # -------------------------------------------------------------------------
144 | # get dataframe variables
145 | opacity, width = list(df_edges['opacity']), list(df_edges['width'])
146 | color = list(df_edges['color'])
147 | # edges plot
148 | for k in range(edges_x.shape[0]):
149 | # switch between 2D / 3D plot
150 | kw_edges = dict(x=edges_x[k, :], y=edges_y[k, :])
151 | if plt_in == '3D':
152 | kw_edges['z'] = edges_z[k, :]
153 | # single line trace
154 | _line = Scatter(
155 | mode='lines', showlegend=False, hoverinfo='none', name='edges',
156 | opacity=opacity[k], line=dict(width=width[k], color=color[k]),
157 | **kw_edges
158 | )
159 | fig.add_trace(_line, **kw_trace)
160 | fig.add_trace(node_trace, **kw_trace)
161 |
162 | # -------------------------------------------------------------------------
163 | # COLORBAR
164 | # -------------------------------------------------------------------------
165 | # edges colorbar (dirty but working solution...)
166 | if cbar:
167 | cbar_trace = go.Scatter(
168 | x=[0.], y=[0.], mode='markers', hoverinfo='none', showlegend=False,
169 | marker=dict(size=[0.], color=list(df_edges['values']),
170 | colorscale=edges_cmap, showscale=True,
171 | colorbar=dict(title=cbar_title, lenmode='fraction', len=0.75,
172 | **kw_cbar))
173 | )
174 | fig.add_trace(cbar_trace)
175 |
176 | fig.update_xaxes(showgrid=False, visible=False, **kw_trace)
177 | fig.update_yaxes(showgrid=False, visible=False, **kw_trace)
178 | if not len(kw_trace):
179 | fig.update_layout(width=900, height=800)
180 |
181 | return fig
182 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | from datetime import date
15 | import sys
16 | import sphinx_bootstrap_theme
17 | import netchos
18 | from sphinx_gallery.sorting import ExplicitOrder
19 | # sys.path.insert(0, os.path.abspath('.'))
20 | sys.path.insert(0, os.path.abspath('sphinxext'))
21 |
22 | import plotly.io as pio
23 |
24 |
25 | # -- Project information -----------------------------------------------------
26 |
27 | project = 'NetCHOS'
28 | td = date.today()
29 | copyright = 'Last updated on %s' % td.isoformat()
30 | author = 'Etienne Combrisson'
31 |
32 | # The full version, including alpha/beta/rc tags
33 | version = netchos.__version__
34 | release = netchos.__version__
35 |
36 |
37 | # -- General configuration ---------------------------------------------------
38 |
39 | # Add any Sphinx extension module names here, as strings. They can be
40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
41 | # ones.
42 | extensions = [
43 | 'sphinx.ext.autodoc',
44 | 'sphinx.ext.doctest',
45 | 'sphinx.ext.intersphinx',
46 | 'sphinx.ext.todo',
47 | 'sphinx.ext.coverage',
48 | 'sphinx.ext.mathjax',
49 | 'sphinx.ext.viewcode',
50 | 'sphinx.ext.githubpages',
51 | 'sphinx.ext.autosummary',
52 | 'sphinx_gallery.gen_gallery',
53 | "sphinx.ext.extlinks",
54 | 'numpydoc'
55 | ]
56 |
57 | # Add any paths that contain templates here, relative to this directory.
58 | templates_path = ['_templates']
59 |
60 | autosummary_generate = True
61 | autodoc_member_order = 'groupwise'
62 | autodoc_default_flags = ['members', 'inherited-members', 'no-undoc-members']
63 |
64 | extlinks = {
65 | "issue": ("https://github.com/brainets/netchos/issues/%s", "IS"),
66 | "pull": ("https://github.com/brainets/netchos/pull/%s", "PR"),
67 | "commit": ("https://github.com/brainets/netchos/commit/%s", "CM"),
68 | }
69 |
70 | # The suffix(es) of source filenames.
71 | # You can specify multiple suffix as a list of string:
72 | #
73 | # source_suffix = ['.rst', '.md']
74 | source_suffix = '.rst'
75 |
76 | # The master toctree document.
77 | master_doc = 'index'
78 |
79 | # The language for content autogenerated by Sphinx. Refer to documentation
80 | # for a list of supported languages.
81 | #
82 | # This is also used if you do content translation via gettext catalogs.
83 | # Usually you set "language" from the command line for these cases.
84 | language = None
85 |
86 | # List of patterns, relative to source directory, that match files and
87 | # directories to ignore when looking for source files.
88 | # This pattern also affects html_static_path and html_extra_path.
89 | exclude_patterns = []
90 |
91 | # The name of the Pygments (syntax highlighting) style to use.
92 | pygments_style = None
93 |
94 |
95 | # -- Options for HTML output -------------------------------------------------
96 |
97 | # The theme to use for HTML and HTML Help pages. See the documentation for
98 | # a list of builtin themes.
99 | #
100 | # html_theme = 'alabaster'
101 | html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
102 | html_theme = 'bootstrap'
103 | html_theme_options = {
104 | 'bootstrap_version': "3",
105 | 'navbar_site_name': "Site",
106 | 'navbar_sidebarrel': False,
107 | 'navbar_pagenav': True,
108 | 'navbar_pagenav_name': "Page",
109 | 'globaltoc_depth': -1,
110 | 'globaltoc_includehidden': "true",
111 | 'source_link_position': "nav",
112 | 'navbar_class': "navbar",
113 | 'bootswatch_theme': "readable",
114 | 'navbar_fixed_top': True,
115 | 'navbar_links': [
116 | ("API", "api"),
117 | ("Examples", "auto_examples/index"),
118 | ],
119 | }
120 |
121 | from plotly.io._sg_scraper import plotly_sg_scraper
122 | image_scrapers = ('matplotlib', plotly_sg_scraper,)
123 |
124 | sphinx_gallery_conf = {
125 | # path to your examples scripts
126 | 'examples_dirs': '../../examples',
127 | 'reference_url': {
128 | 'netchos': None,
129 | 'matplotlib': 'https://matplotlib.org',
130 | 'numpy': 'http://docs.scipy.org/doc/numpy',
131 | 'scipy': 'http://docs.scipy.org/doc/scipy/reference',
132 | 'pandas': 'https://pandas.pydata.org/pandas-docs/stable',
133 | },
134 | 'gallery_dirs': 'auto_examples',
135 | 'backreferences_dir': 'generated',
136 | 'filename_pattern': '/plot_|sim_',
137 | 'image_scrapers': image_scrapers,
138 | # 'default_thumb_file': 'source/_static/netchos.png',
139 | }
140 |
141 | numpydoc_show_class_members = False
142 | # numpydoc_class_members_toctree = False
143 | # numpydoc_use_blockquotes = False
144 |
145 | # Theme options are theme-specific and customize the look and feel of a theme
146 | # further. For a list of options available for each theme, see the
147 | # documentation.
148 | #
149 | # html_theme_options = {}
150 |
151 | # Add any paths that contain custom static files (such as style sheets) here,
152 | # relative to this directory. They are copied after the builtin static files,
153 | # so a file named "default.css" will overwrite the builtin "default.css".
154 | html_static_path = ['_static']
155 |
156 | # Custom sidebar templates, must be a dictionary that maps document names
157 | # to template names.
158 | #
159 | # The default sidebars (for documents that don't match any pattern) are
160 | # defined by theme itself. Builtin themes are using these templates by
161 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
162 | # 'searchbox.html']``.
163 | #
164 | # html_sidebars = {}
165 |
166 |
167 | # -- Options for HTMLHelp output ---------------------------------------------
168 |
169 | # Output file base name for HTML help builder.
170 | htmlhelp_basename = 'netchosdoc'
171 |
172 | # The name of an image file (relative to this directory) to place at the top
173 | # of the sidebar.
174 | # html_logo = '_static/netchos_128x128.png'
175 |
176 | # The name of an image file (relative to this directory) to use as a favicon of
177 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or
178 | # 32x32 pixels large.
179 | # html_favicon = '_static/favicon.ico'
180 |
181 | html_show_sourcelink = False
182 |
183 | # -- Options for LaTeX output ------------------------------------------------
184 |
185 | latex_elements = {
186 | # The paper size ('letterpaper' or 'a4paper').
187 | #
188 | # 'papersize': 'letterpaper',
189 |
190 | # The font size ('10pt', '11pt' or '12pt').
191 | #
192 | # 'pointsize': '10pt',
193 |
194 | # Additional stuff for the LaTeX preamble.
195 | #
196 | # 'preamble': '',
197 |
198 | # Latex figure (float) alignment
199 | #
200 | # 'figure_align': 'htbp',
201 | }
202 |
203 | # Grouping the document tree into LaTeX files. List of tuples
204 | # (source start file, target name, title,
205 | # author, documentclass [howto, manual, or own class]).
206 | latex_documents = [
207 | (master_doc, 'netchos.tex', 'netchos Documentation',
208 | 'Etienne Combrisson', 'manual'),
209 | ]
210 |
211 |
212 | # -- Options for manual page output ------------------------------------------
213 |
214 | # One entry per manual page. List of tuples
215 | # (source start file, name, description, authors, manual section).
216 | man_pages = [
217 | (master_doc, 'netchos', 'netchos Documentation',
218 | [author], 1)
219 | ]
220 |
221 |
222 | # -- Options for Texinfo output ----------------------------------------------
223 |
224 | # Grouping the document tree into Texinfo files. List of tuples
225 | # (source start file, target name, title, author,
226 | # dir menu entry, description, category)
227 | texinfo_documents = [
228 | (master_doc, 'netchos', 'netchos Documentation',
229 | author, 'netchos', 'One line description of project.',
230 | 'Miscellaneous'),
231 | ]
232 |
233 |
234 | # -- Options for Epub output -------------------------------------------------
235 |
236 | # Bibliographic Dublin Core info.
237 | epub_title = project
238 |
239 | # The unique identifier of the text. This can be a ISBN number
240 | # or the project homepage.
241 | #
242 | # epub_identifier = ''
243 |
244 | # A unique identification for the text.
245 | #
246 | # epub_uid = ''
247 |
248 | # A list of files that should not be packed into the epub file.
249 | epub_exclude_files = ['search.html']
250 |
251 |
252 | # -- Extension configuration -------------------------------------------------
253 |
254 | # -- Options for intersphinx extension ---------------------------------------
255 |
256 | # Example configuration for intersphinx: refer to the Python standard library.
257 | intersphinx_mapping = {'https://docs.python.org/': None}
258 |
259 | # -- Options for todo extension ----------------------------------------------
260 |
261 | # If true, `todo` and `todoList` produce output, else they produce nothing.
262 | todo_include_todos = True
--------------------------------------------------------------------------------
/netchos/circular.py:
--------------------------------------------------------------------------------
1 | """Circular plotting layout."""
2 | import pandas as pd
3 | import numpy as np
4 |
5 | from netchos.io import io_to_df
6 | from netchos.utils import (normalize, extract_df_cols, prepare_to_plot,
7 | categorize)
8 |
9 |
10 | def circular(
11 | conn, nodes_data=None, nodes_name=None, nodes_color=None,
12 | nodes_size=None, nodes_size_min=1, nodes_size_max=10, nodes_cmap='plasma',
13 | nodes_text=None, nodes_text_offset=1., nodes_text_size=12, edges_min=None,
14 | edges_max=None, edges_width_min=.5, edges_width_max=6,
15 | edges_opacity_min=0.1, edges_opacity_max=1., edges_cmap='plasma',
16 | categories=None, categories_color=None, cbar=True, cbar_title='Edges',
17 | directed=False, angle_start=90, angle_range=360, fig=None, kw_trace={},
18 | kw_cbar={}):
19 | """Network plotting within a circular layout.
20 |
21 | Parameters
22 | ----------
23 | conn : array_like or DataFrame or DataArray
24 | 2D Connectivity matrix of shape (n_rows, n_cols). If conn is a
25 | DataFrame or a DataArray, the indexes and columns are used for the conn
26 | and y tick labels
27 | nodes_data : pd.DataFrame
28 | DataFrame that can contains nodes informations (e.g the name of the
29 | nodes, the x, y and z coordinates, values assign to the nodes etc.)
30 | nodes_name : list, array_like, str | None
31 | List of names of the nodes. Alternatively, if `nodes_data` is provided,
32 | a string referring to a column name can be provided instead
33 | nodes_color, nodes_size : list, array_like, str | None
34 | List of values assign to each node in order to modulate respectively
35 | the color and the size of the nodes. If None, the degree of each node
36 | is going to be used instead. Alternatively, if `nodes_data` is
37 | provided, a string referring to a column name can be provided instead
38 | nodes_size_min, nodes_size_max : float | 1, 30
39 | Respectively, the minimum and maximum size to use for the markers
40 | nodes_cmap : str | 'plasma'
41 | Colormap to use in order to infer the color of each node
42 | nodes_text : dict | None
43 | Text to display at each node. It should be a dict where the keys are
44 | the nodes names and the values the text to display
45 | nodes_text_offset : float | 1.
46 | Floating point indicating the offset to apply to the name of each node
47 | nodes_text_size : float | 12
48 | Font size for the nodes names
49 | edges_min, edges_max : float | None
50 | Respectively the minimum and maximum to use for clipping edges values
51 | edges_width_min, edges_width_max : float | .5, .8
52 | Respectively the minimum and maximum width to use fot the edges
53 | edges_opacity_min, edges_opacity_max : float | 0.1, 1.
54 | Respectively the minimum and maximum opacity for edges
55 | edges_cmap : str | 'plasma'
56 | Colormap to use to infer the color of each edge
57 | categories : dict, str | None
58 | Nodes categories. If a dict is passed, the keys should corresponds
59 | to the name of the nodes and the values to the category name.
60 | Alternatively, if `nodes_data` is provided, a string referring to a
61 | column name can be provided instead
62 | categories_color : dict | None
63 | Text color following ctagories name. It should be a dict where the keys
64 | refer the the values of the input `categories` and the values the color
65 | to use
66 | cbar : bool | True
67 | Add a colorbar to the plot.
68 | cbar_title : str | 'Edges
69 | Default colorbar title
70 | directed : bool | False
71 | Specify if the graph is undirected (False) or directed (True)
72 | angle_start : float | 90
73 | The angle (in degree) at which to start setting nodes names
74 | angle_range : float | 360
75 | Angle range coverered (in degree). For example, if 180 is given, half
76 | of the circle is going to be displayed
77 | fig : go.Figure | None
78 | plotly.graph_objects.Figure object
79 | kw_trace : dict | {}
80 | Additional arguments to pass to the
81 | plotly.graph_objects.Figure.add_trace method
82 |
83 | Returns
84 | -------
85 | fig : go.Figure | None
86 | A plotly.graph_objects.Figure object
87 | """
88 | import plotly.graph_objects as go
89 |
90 | # -------------------------------------------------------------------------
91 | # I/O
92 | # -------------------------------------------------------------------------
93 | # connectivity matrix conversion
94 | conn = io_to_df(conn, xr_pivot=True)
95 |
96 | # get node names and coordinates in case of dataframe
97 | kw_nodes = dict(nodes_name=nodes_name, nodes_size=nodes_size,
98 | nodes_color=nodes_color)
99 | if isinstance(nodes_data, pd.DataFrame):
100 | kw_nodes = extract_df_cols(nodes_data, **kw_nodes)
101 |
102 | # get useful variables for plotting
103 | df_nodes, df_edges = prepare_to_plot(
104 | conn, nodes_size_min=nodes_size_min, nodes_size_max=nodes_size_max,
105 | nodes_text=nodes_text, edges_min=edges_min, edges_max=edges_max,
106 | edges_width_min=edges_width_min, edges_width_max=edges_width_max,
107 | edges_opacity_min=edges_opacity_min,
108 | edges_opacity_max=edges_opacity_max, directed=directed,
109 | edges_cmap=edges_cmap, edges_sorted=True, edges_rm_missing=True,
110 | **kw_nodes
111 | )
112 |
113 | # extract categories when combined with dataframe
114 | nodes_name = df_nodes['name'].values
115 | if isinstance(categories, str) and isinstance(nodes_data, pd.DataFrame):
116 | categories = {k: v for k, v in zip(nodes_name, nodes_data[categories])}
117 | n_nodes = len(df_nodes)
118 |
119 | # -------------------------------------------------------------------------
120 | # LAYOUT
121 | # -------------------------------------------------------------------------
122 | # degree to rad conversion
123 | angle_start_rad = np.deg2rad(angle_start)
124 | angle_range_rad = np.deg2rad(angle_range)
125 |
126 | # categories
127 | if isinstance(categories, dict):
128 | cuts = np.r_[categorize(nodes_name, categories), len(nodes_name)]
129 | else:
130 | cuts = []
131 | n_cat = len(cuts)
132 |
133 | # compute position in circle
134 | r = np.full((n_nodes,), 10.)
135 | angle = np.linspace(0, angle_range_rad, n_nodes + 1 + n_cat)[0:-1]
136 | delta = (angle[1] - angle[0]) / 2.
137 | angle = angle + angle_start_rad + 2 * delta
138 | if n_cat:
139 | angle = np.delete(angle, cuts + np.arange(n_cat))
140 |
141 | # infer x and y positions
142 | x = np.multiply(r, np.cos(angle))
143 | y = np.multiply(r, np.sin(angle))
144 |
145 | # node names position
146 | x_names = np.multiply(r + nodes_text_offset, np.cos(angle))
147 | y_names = np.multiply(r + nodes_text_offset, np.sin(angle))
148 |
149 | # -------------------------------------------------------------------------
150 | # PLOT VARIABLES
151 | # -------------------------------------------------------------------------
152 | if fig is None:
153 | fig = go.Figure()
154 |
155 | sizeref = np.max(df_nodes['size_plt']) / nodes_size_max ** 2
156 |
157 | # prepare hover data
158 | hovertemplate = (
159 | "Node : %{text}
Size : %{customdata[0]:.3f}
"
160 | "Color : %{customdata[1]:.3f}
"
161 | "Degree : %{customdata[2]}")
162 |
163 | # hover custom data
164 | customdata = np.stack(
165 | (df_nodes['size'], df_nodes['color'], df_nodes['degree']), axis=-1)
166 |
167 | # -------------------------------------------------------------------------
168 | # NODES PLOT
169 | # -------------------------------------------------------------------------
170 | trace = go.Scatter(
171 | x=x, y=y, mode='markers', text=list(df_nodes['name']),
172 | hovertemplate=hovertemplate, customdata=customdata, name='Nodes',
173 | showlegend=False, marker=dict(
174 | sizemode='area', color=df_nodes['color_plt'], sizeref=sizeref,
175 | size=df_nodes['size_plt'], colorscale=nodes_cmap, opacity=1.
176 | ),
177 | )
178 | fig.add_trace(trace, **kw_trace)
179 |
180 | # -------------------------------------------------------------------------
181 | # NODES NAMES PLOT
182 | # -------------------------------------------------------------------------
183 | nodes_text = df_nodes['text']
184 | for k in range(n_nodes):
185 | # rotation offset
186 | off = np.pi if x_names[k] < 0 else 0.
187 |
188 | # categorical colors
189 | if isinstance(categories, dict) and isinstance(categories_color, dict):
190 | category = categories[nodes_name[k]]
191 | anot_color = categories_color[category]
192 | else:
193 | anot_color = None
194 |
195 | fig.add_annotation(
196 | x=x_names[k], y=y_names[k], text=nodes_text[k], showarrow=False,
197 | textangle=np.rad2deg(off - angle[k]),
198 | font=dict(size=nodes_text_size, color=anot_color), **kw_trace
199 | )
200 |
201 | # -------------------------------------------------------------------------
202 | # EDGES PLOT
203 | # -------------------------------------------------------------------------
204 | width, color = list(df_edges['width']), list(df_edges['color'])
205 | opacity = list(df_edges['opacity'])
206 | edges_ref = np.c_[df_edges['s'].values, df_edges['t'].values]
207 | shapes = []
208 | for n_p, (k, i) in enumerate(edges_ref):
209 | # path creation
210 | _path = dict(type='path', path=f"M {x[k]},{y[k]} Q 0,0 {x[i]},{y[i]}",
211 | line_color=color[n_p], line=dict(width=width[n_p]),
212 | opacity=opacity[n_p], layer="below")
213 | fig.add_shape(_path, **kw_trace)
214 |
215 | # -------------------------------------------------------------------------
216 | # COLORBAR PLOT
217 | # -------------------------------------------------------------------------
218 | if cbar:
219 | cbar_trace = go.Scatter(
220 | x=[0.], y=[0.], mode='markers', hoverinfo='none', showlegend=False,
221 | marker=dict(
222 | size=[0.], color=list(df_edges['colorbar']),
223 | colorscale=edges_cmap, showscale=True,
224 | colorbar=dict(title=cbar_title, lenmode='fraction', len=0.75,
225 | **kw_cbar)
226 | )
227 | )
228 | fig.add_trace(cbar_trace, **kw_trace)
229 |
230 | axis = dict(showgrid=False, visible=False, scaleanchor="x", scaleratio=1)
231 | fig.update_xaxes(**axis, **kw_trace)
232 | fig.update_yaxes(**axis, **kw_trace)
233 | if not len(kw_trace):
234 | width = min(angle_range * 500 / 180, 800)
235 | fig.update_layout(width=width, height=800)
236 |
237 | return fig
238 |
--------------------------------------------------------------------------------