├── 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 | --------------------------------------------------------------------------------