├── .flake8 ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── Interactive Surface Visualization Demo.ipynb ├── LICENSE.md ├── Makefile ├── README.md ├── demo_update.ipynb ├── docs ├── Makefile ├── _static │ └── niwidgets-500x500.png ├── api.rst ├── api │ ├── niwidgets.NiftiWidget.rst │ ├── niwidgets.StreamlineWidget.rst │ └── niwidgets.SurfaceWidget.rst ├── conf.py ├── img │ └── example.gif ├── index.md ├── installation.rst ├── logo.png ├── old-logo.png └── requirements.txt ├── index.ipynb ├── pyproject.toml ├── report.ipynb ├── src └── niwidgets │ ├── __init__.py │ ├── colormaps.py │ ├── controls.py │ ├── data │ ├── T1.nii.gz │ ├── cc400_roi_atlas.nii │ ├── cognitive control_pFgA_z.nii.gz │ ├── cognitive control_pFgA_z_FDR_0.01.nii.gz │ ├── example_surfaces │ │ ├── aparc.annot.ctab │ │ ├── lh.aparc.annot │ │ ├── lh.area │ │ ├── lh.curv │ │ ├── lh.inflated │ │ └── lh.thickness │ └── streamlines.trk │ ├── exampledata.py │ ├── niwidget_surface.py │ ├── niwidget_volume.py │ ├── streamlines.py │ └── version.py └── tests ├── conftest.py ├── test_surface.py └── test_volume.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, W503 3 | max-line-length = 80 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # notebooks 10 | .ipnyb_checkpoints 11 | 12 | # mac files 13 | *.DS_Store 14 | 15 | # pytest 16 | .pytest_cache 17 | 18 | # Distribution / packaging 19 | .Python 20 | env/ 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | .pytest_cache 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *,cover 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | \.ipynb_checkpoints/ 71 | docs/_build 72 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | builder: html 11 | configuration: docs/conf.py 12 | 13 | build: 14 | image: latest 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: [] 18 | 19 | # Optionally set the version of Python and requirements required to build your docs 20 | python: 21 | version: 3.7 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: pip 25 | path: . 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | stages: 4 | - "Static code analysis" 5 | - test 6 | - deploy 7 | python: 8 | - "3.5" 9 | - "3.6" 10 | - "3.7" 11 | notifications: 12 | email: false 13 | install: 14 | - pip install --upgrade pip 15 | - pip install poetry 16 | - poetry install 17 | script: 18 | # run tests, and get coverage: 19 | - poetry run python -m pytest --cov=src/ 20 | # test that the notebooks all run smoothly 21 | # - find docs -name '*.ipynb' -o -name 'examples/*.ipynb' | xargs poetry run jupyter nbconvert --to notebook --execute 22 | # submit coverage report to coveralls 23 | - poetry run coveralls 24 | jobs: 25 | include: 26 | - stage: "Static code analysis" 27 | name: "black formatting test" 28 | script: 29 | - black --check src/ 30 | python: "3.7" 31 | install: 32 | - pip install -qq black 33 | - stage: "Static code analysis" 34 | name: "flake8 check" 35 | script: 36 | - flake8 src/ 37 | python: "3.7" 38 | install: 39 | - pip install -qq flake8 40 | - stage: deploy 41 | name: "Deploy to pypi" 42 | if: tag IS present 43 | script: 44 | - poetry build 45 | - poetry publish --username $PYPI_USER --password $PYPI_PASSWORD 46 | python: "3.7" 47 | -------------------------------------------------------------------------------- /Interactive Surface Visualization Demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Interactive Surface Visualization Demo Notebook\n", 8 | "\n", 9 | " developed by Murat Bilgel, Benjamin Ely, Melanie Ganz, Krisanne Litinas, and Andrea Shafer\n", 10 | " Heavily indebted to Satra Ghosh, Chris Holdgraf, Anisha Keshavan, and Tal Yarkoni\n", 11 | " Builds on niwidgets\n", 12 | "\n" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 1, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "%matplotlib inline\n", 22 | "%load_ext autoreload\n", 23 | "%autoreload 2\n", 24 | "import os\n", 25 | "import sys\n", 26 | "from niwidgets import niwidget_surface\n", 27 | "from niwidgets.exampledata import examplesurface, exampleoverlays\n", 28 | "from pathlib import Path" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 4, 34 | "metadata": {}, 35 | "outputs": [ 36 | { 37 | "data": { 38 | "application/vnd.jupyter.widget-view+json": { 39 | "model_id": "4546a2e205324411baac3eb08f7bf06e", 40 | "version_major": 2, 41 | "version_minor": 0 42 | }, 43 | "text/html": [ 44 | "

Failed to display Jupyter Widget of type interactive.

\n", 45 | "

\n", 46 | " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", 47 | " that the widgets JavaScript is still loading. If this message persists, it\n", 48 | " likely means that the widgets JavaScript library is either not installed or\n", 49 | " not enabled. See the Jupyter\n", 50 | " Widgets Documentation for setup instructions.\n", 51 | "

\n", 52 | "

\n", 53 | " If you're reading this message in another notebook frontend (for example, a static\n", 54 | " rendering on GitHub or NBViewer),\n", 55 | " it may mean that your frontend doesn't currently support widgets.\n", 56 | "

\n" 57 | ], 58 | "text/plain": [ 59 | "interactive(children=(IntSlider(value=0, description='frame', max=3), Dropdown(description='colormap', index=74, options=('viridis', 'Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'), value='summer'), Output()), _dom_classes=('widget-interact',))" 60 | ] 61 | }, 62 | "metadata": {}, 63 | "output_type": "display_data" 64 | }, 65 | { 66 | "data": { 67 | "application/vnd.jupyter.widget-view+json": { 68 | "model_id": "fddb214fc89b4e7dad8bbdc5e855d0c2", 69 | "version_major": 2, 70 | "version_minor": 0 71 | }, 72 | "text/html": [ 73 | "

Failed to display Jupyter Widget of type Figure.

\n", 74 | "

\n", 75 | " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", 76 | " that the widgets JavaScript is still loading. If this message persists, it\n", 77 | " likely means that the widgets JavaScript library is either not installed or\n", 78 | " not enabled. See the Jupyter\n", 79 | " Widgets Documentation for setup instructions.\n", 80 | "

\n", 81 | "

\n", 82 | " If you're reading this message in another notebook frontend (for example, a static\n", 83 | " rendering on GitHub or NBViewer),\n", 84 | " it may mean that your frontend doesn't currently support widgets.\n", 85 | "

\n" 86 | ], 87 | "text/plain": [ 88 | "Figure(camera_center=[0.0, 0.0, 0.0], camera_fov=1.0, height=600, matrix_projection=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], matrix_world=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], meshes=[Mesh(color=array([[ 0. , 0.5, 0.4],\n", 89 | " [ 0. , 0.5, 0.4],\n", 90 | " [ 0. , 0.5, 0.4],\n", 91 | " ..., \n", 92 | " [ 0. , 0.5, 0.4],\n", 93 | " [ 0. , 0.5, 0.4],\n", 94 | " [ 0. , 0.5, 0.4]]), texture=None, triangles=array([[ 0, 1, 3],\n", 95 | " [ 4, 3, 1],\n", 96 | " [ 0, 91, 1],\n", 97 | " ..., \n", 98 | " [123307, 123906, 123895],\n", 99 | " [123906, 123907, 123895],\n", 100 | " [123885, 124986, 124987]], dtype=uint32), x=array([ 18.7413578 , 18.37849426, 19.00789833, ..., 17.78539658,\n", 101 | " 17.8218174 , 17.95105362]), y=array([-127.09669495, -127.08947754, -127.14855194, ..., 96.06409454,\n", 102 | " 96.46263123, 96.3392868 ]), z=array([-48.42454529, -48.42292786, -48.9959259 , ..., 44.24810028,\n", 103 | " 44.08300781, 44.18468475]))], style={'axes': {'color': 'black', 'label': {'color': 'black'}, 'ticklabel': {'color': 'black'}, 'visible': False}, 'background-color': 'white', 'box': {'visible': False}}, tf=None, width=600, xlim=[-100.0, 100.0], ylim=[-127.3917007446289, 127.3917007446289], zlim=[-100.0, 100.0])" 104 | ] 105 | }, 106 | "metadata": {}, 107 | "output_type": "display_data" 108 | } 109 | ], 110 | "source": [ 111 | "my_widget = niwidget_surface.SurfaceWidget(examplesurface, exampleoverlays)\n", 112 | "\n", 113 | "my_widget.surface_plotter(showZeroes=True)" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [] 122 | } 123 | ], 124 | "metadata": { 125 | "kernelspec": { 126 | "display_name": "Python 3", 127 | "language": "python", 128 | "name": "python3" 129 | }, 130 | "language_info": { 131 | "codemirror_mode": { 132 | "name": "ipython", 133 | "version": 3 134 | }, 135 | "file_extension": ".py", 136 | "mimetype": "text/x-python", 137 | "name": "python", 138 | "nbconvert_exporter": "python", 139 | "pygments_lexer": "ipython3", 140 | "version": "3.6.1" 141 | } 142 | }, 143 | "nbformat": 4, 144 | "nbformat_minor": 2 145 | } 146 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Niwidgets developers 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = niwidgets 8 | SOURCEDIR = ./doc 9 | GH_PAGES_SOURCES = Makefile doc niwidgets index.ipynb 10 | BUILDDIR = doc/_build 11 | # BUILDDIR = .. 12 | 13 | # Put it first so that "make" without argument is like "make help". 14 | help: 15 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 16 | 17 | .PHONY: help Makefile 18 | 19 | # Catch-all target: route all unknown targets to Sphinx using the new 20 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 21 | %: Makefile 22 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 23 | 24 | gh-pages: 25 | git checkout gh-pages 26 | rm -rf * 27 | git checkout master $(GH_PAGES_SOURCES) 28 | git reset HEAD 29 | cp ./index.ipynb doc/examples.ipynb 30 | make html 31 | mv -fv $(BUILDDIR)/html/* ./ 32 | rm -rf $(SOURCEDIR) build 33 | > .nojekyll 34 | git add -A 35 | git commit -m "Generated gh-pages automatically." 36 | git push origin gh-pages 37 | git checkout master 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neuroimaging Widgets (`niwidgets`) 2 | 3 | This repository is supposed to provide easy and general wrappers to display 4 | interactive widgets that visualise standard-format neuroimaging data, using new 5 | functions and standard functions from other libraries. It looks like this: 6 | 7 | ![](https://thumbs.gfycat.com/ExcitableReflectingLcont-size_restricted.gif) 8 | 9 | Install via: 10 | 11 | ``` 12 | pip install niwidgets 13 | ``` 14 | 15 | Or, to get the most up-to-date development version from github: 16 | 17 | ``` 18 | pip install git+git://github.com/nipy/niwidgets/ 19 | ``` 20 | 21 | It requires nibabel and nilearn: 22 | 23 | ``` 24 | pip install nibabel nilearn 25 | ``` 26 | 27 | Check out the examples using the code in this notebook here: 28 | https://github.com/nipy/niwidgets/blob/master/index.ipynb (you need to run the 29 | notebook on your local machine to use the interactive features). 30 | 31 | or using binder here: 32 | https://mybinder.org/v2/gh/nipy/niwidgets/master?filepath=index.ipynb 33 | 34 | ### Usage: 35 | 36 | There are currently three supported widgets: 37 | 38 | 1. Volume widgets. This widget is primarily designed to mimic existing tools 39 | such as , but it also allows you to wrap plots from the `nilearn` 40 | plotting library to make them interactive. 41 | 42 | 2. Surface widgets. This widget takes freesurfer-generated volume files and 43 | turns them into widgets using the `ipyvolume` library. It allows you to add 44 | different overlays for the surface files. 45 | 46 | 3. Streamline widgets. This widget accepts `.trk` files and displays the tracts 47 | using `ipyvolume`. 48 | 49 | To see how to use these widgets, please check the 50 | [documentation](https://nipy.org/niwidgets). 51 | 52 | As an example of how you might generate a Volume widget: 53 | 54 | ``` 55 | from niwidgets import NiftiWidget 56 | 57 | my_widget = NiftiWidget('./path/to/file.nii') 58 | ``` 59 | 60 | You can then create a plot either with the default nifti plotter: 61 | 62 | ``` 63 | my_widget.nifti_plotter() 64 | ``` 65 | 66 | This will give you sliders to slice through the image, and an option to set the 67 | colormap. 68 | 69 | You can also provide your own plotting function: 70 | 71 | ``` 72 | import nilearn.plotting as nip 73 | 74 | my_widget.nifti_plotter(plotting_func=nip.plot_glass_brain) 75 | ``` 76 | 77 | By default, this will give you the following interactive features: - 78 | selecting a colormap - if supported by the plotting function, x-y-x 79 | sliders (e.g. for `nip.plot_img`) 80 | 81 | 82 | You can, however, always provide features you would like to have interactive 83 | yourself. This follows the normal ipywidgets format. For example, if you provide 84 | a list of strings for a keyword argument, this becomes a drop-down menu. If you 85 | provide a tuple of two numbers, this becomes a slider. Take a look at some 86 | examples we have in [this 87 | notebook](https://github.com/janfreyberg/niwidgets/blob/master/visualisation_wrapper.ipynb) 88 | (you need to run the notebook on your local machine to use the interactive 89 | features). 90 | 91 | Hopefully we will be able to add more default interactive features in the 92 | future, as well as plotting of other data (such as surface projections). If you 93 | have any suggestions for plot features to be added, please let us know - or add 94 | them yourself and create a pull request! 95 | 96 | ## Development 97 | 98 | ![](https://travis-ci.org/nipy/niwidgets.svg?branch=master) 99 | 100 | ### Contributing 101 | 102 | Please contribute! When writing new widgets, please make sure you include 103 | example data that allows users to try a widget without having to munge their 104 | data into the right format first. 105 | 106 | Please also make sure you write a test for your new widget. It's hard to test 107 | jupyter widgets, but it would be great if you could at least write a test that 108 | "instantiates" a widget. This allows us to maintain a stable release. 109 | 110 | ### Development installation 111 | 112 | As always with pip packages, you can install a _"development"_ version of this 113 | package by cloning the git repository and installing it via `pip install -e 114 | /path/to/package`. 115 | 116 | ### Updating the documentation 117 | 118 | To update the documentation, you can do the following things: 119 | 120 | - Make your changes on a separate branch, such as DOC/update-api-documentation. 121 | - Merge your branch into master Make sure you have the packages in 122 | - `doc-requirements.txt` installed Run `make gh-pages` in the root directory of 123 | - the repository 124 | 125 | This should run sphinx to generate the documentation, push it to the gh-pages 126 | branch, and then revert to master. 127 | 128 | --- 129 | 130 | _Developed by [Jan Freyberg](http://www.twitter.com/janfreyberg), [Bjoern 131 | Soergel](http://www.ast.cam.ac.uk/~bs538/), [Satrajit 132 | Ghosh](https://github.com/satra), [Melanie 133 | Ganz](https://github.com/melanieganz), [Murat 134 | Bilgel](https://github.com/bilgelm), [Ariel Rokem](https://github.com/arokem), 135 | and [elyb01](https://github.com/elyb01)._ 136 | -------------------------------------------------------------------------------- /demo_update.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "scrolled": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stderr", 12 | "output_type": "stream", 13 | "text": [ 14 | "/Users/janfreyberg/anaconda3/envs/py36/lib/python3.6/site-packages/h5py/__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.\n", 15 | " from ._conv import register_converters as _register_converters\n" 16 | ] 17 | } 18 | ], 19 | "source": [ 20 | "from niwidgets.niwidget_volume import VolumeWidget, NiftiWidget\n", 21 | "from niwidgets.exampledata import examplet1\n", 22 | "from IPython import display\n" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "The new volume widget works slightly differently: It doesn't rely on `widgets.interact`, and therefore allows more control over the interactive elements, and what happens when they change. For example, we can make cool play buttons to move through the brain smoothly.\n", 30 | "\n", 31 | "You also don't need to call a plotter / rendering function anymore (although you can)." 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 3, 37 | "metadata": { 38 | "scrolled": false 39 | }, 40 | "outputs": [ 41 | { 42 | "data": { 43 | "application/vnd.jupyter.widget-view+json": { 44 | "model_id": "7932ddd4bdcd4fd883497ad46026882a", 45 | "version_major": 2, 46 | "version_minor": 0 47 | }, 48 | "text/plain": [ 49 | "VBox(children=(Box(children=(PlaySlider(children=(Label(value='X: '), Play(value=45, interval=300, max=90), In…" 50 | ] 51 | }, 52 | "metadata": {}, 53 | "output_type": "display_data" 54 | } 55 | ], 56 | "source": [ 57 | "widget = VolumeWidget(examplet1, colormap='viridis')\n", 58 | "\n", 59 | "widget # equivalent to widget.render()" 60 | ] 61 | }, 62 | { 63 | "cell_type": "markdown", 64 | "metadata": {}, 65 | "source": [ 66 | "Amongst other things, you can now choose whether to follow radiological standards (left-right mirror images). Turned on by default, you can either untick the checkbox, or pass it as an argument to the widget:" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 4, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "data": { 76 | "application/vnd.jupyter.widget-view+json": { 77 | "model_id": "b7df1220e0104e8f9bd32971155cfe17", 78 | "version_major": 2, 79 | "version_minor": 0 80 | }, 81 | "text/plain": [ 82 | "VBox(children=(Box(children=(PlaySlider(children=(Label(value='X: '), Play(value=45, interval=300, max=90), In…" 83 | ] 84 | }, 85 | "metadata": {}, 86 | "output_type": "display_data" 87 | } 88 | ], 89 | "source": [ 90 | "widget = VolumeWidget(examplet1, orient_radiology=False)\n", 91 | "\n", 92 | "widget" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "In addition, composing the widget from scratch means that the control elements and plots can re-flow if the page isn't large enough. Try shrinking your browser window, or adjusting the figure size:" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 5, 105 | "metadata": { 106 | "scrolled": false 107 | }, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "application/vnd.jupyter.widget-view+json": { 112 | "model_id": "883c5edfafd747fcaa8427d5c0926ba0", 113 | "version_major": 2, 114 | "version_minor": 0 115 | }, 116 | "text/plain": [ 117 | "VBox(children=(Box(children=(PlaySlider(children=(Label(value='X: '), Play(value=45, interval=300, max=90), In…" 118 | ] 119 | }, 120 | "metadata": {}, 121 | "output_type": "display_data" 122 | } 123 | ], 124 | "source": [ 125 | "widget = VolumeWidget(examplet1, figsize=(6, 6))\n", 126 | "\n", 127 | "widget" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "metadata": {}, 133 | "source": [ 134 | "Lastly, making the control elements part of the class means you can interact with it more like a normal python object, e.g. get and set the elements of the graph as python attributes:" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": 6, 140 | "metadata": {}, 141 | "outputs": [ 142 | { 143 | "name": "stdout", 144 | "output_type": "stream", 145 | "text": [ 146 | "[45, 54, 45]\n", 147 | "viridis\n", 148 | "summer\n" 149 | ] 150 | } 151 | ], 152 | "source": [ 153 | "print(widget.indices)\n", 154 | "print(widget.colormap)\n", 155 | "widget.colormap = 'summer'\n", 156 | "print(widget.colormap)" 157 | ] 158 | } 159 | ], 160 | "metadata": { 161 | "kernelspec": { 162 | "display_name": "Python 3", 163 | "language": "python", 164 | "name": "python3" 165 | }, 166 | "language_info": { 167 | "codemirror_mode": { 168 | "name": "ipython", 169 | "version": 3 170 | }, 171 | "file_extension": ".py", 172 | "mimetype": "text/x-python", 173 | "name": "python", 174 | "nbconvert_exporter": "python", 175 | "pygments_lexer": "ipython3", 176 | "version": "3.6.3" 177 | }, 178 | "toc": { 179 | "nav_menu": {}, 180 | "number_sections": true, 181 | "sideBar": true, 182 | "skip_h1_title": false, 183 | "toc_cell": false, 184 | "toc_position": {}, 185 | "toc_section_display": "block", 186 | "toc_window_display": false 187 | } 188 | }, 189 | "nbformat": 4, 190 | "nbformat_minor": 2 191 | } 192 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = niwidgets 8 | SOURCEDIR = . 9 | GH_PAGES_SOURCES = Makefile doc niwidgets index.ipynb 10 | BUILDDIR = _build 11 | # BUILDDIR = .. 12 | 13 | # Put it first so that "make" without argument is like "make help". 14 | help: 15 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 16 | 17 | .PHONY: help Makefile 18 | 19 | # Catch-all target: route all unknown targets to Sphinx using the new 20 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 21 | %: Makefile 22 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 23 | -------------------------------------------------------------------------------- /docs/_static/niwidgets-500x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/docs/_static/niwidgets-500x500.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | There are three widget classes: 5 | 6 | .. currentmodule:: niwidgets 7 | 8 | .. autosummary:: 9 | :toctree: api 10 | 11 | NiftiWidget 12 | SurfaceWidget 13 | StreamlineWidget 14 | -------------------------------------------------------------------------------- /docs/api/niwidgets.NiftiWidget.rst: -------------------------------------------------------------------------------- 1 | niwidgets.NiftiWidget 2 | ===================== 3 | 4 | 5 | .. currentmodule:: niwidgets 6 | 7 | 8 | .. autoclass:: NiftiWidget 9 | 10 | 11 | ----- 12 | 13 | .. rubric:: Methods 14 | 15 | These are the methods that are added by niwidgets. All methods, including the inherited ones, are at the end of the page. 16 | 17 | .. autoautosummary:: niwidgets.NiftiWidget 18 | :methods: 19 | 20 | 21 | ----- 22 | 23 | .. rubric:: Methods Details 24 | 25 | 26 | .. raw:: html 27 | 28 |
29 | 30 | .. autoclass:: NiftiWidget 31 | :members: 32 | 33 | .. raw:: html 34 | 35 |
36 | -------------------------------------------------------------------------------- /docs/api/niwidgets.StreamlineWidget.rst: -------------------------------------------------------------------------------- 1 | niwidgets.StreamlineWidget 2 | ========================== 3 | 4 | 5 | .. currentmodule:: niwidgets 6 | 7 | 8 | .. autoclass:: StreamlineWidget 9 | 10 | 11 | ----- 12 | 13 | .. rubric:: Methods 14 | 15 | Streamline widget methods: 16 | 17 | .. autoautosummary:: niwidgets.StreamlineWidget 18 | :methods: 19 | 20 | 21 | ----- 22 | 23 | .. rubric:: Methods Details 24 | 25 | 26 | .. raw:: html 27 | 28 |
29 | 30 | .. autoclass:: StreamlineWidget 31 | :members: 32 | 33 | .. raw:: html 34 | 35 |
36 | -------------------------------------------------------------------------------- /docs/api/niwidgets.SurfaceWidget.rst: -------------------------------------------------------------------------------- 1 | niwidgets.SurfaceWidget 2 | ======================= 3 | 4 | 5 | .. currentmodule:: niwidgets 6 | 7 | 8 | .. autoclass:: SurfaceWidget 9 | 10 | 11 | ----- 12 | 13 | .. rubric:: Methods 14 | 15 | Surface widget methods: 16 | 17 | .. autoautosummary:: niwidgets.SurfaceWidget 18 | :methods: 19 | 20 | 21 | ----- 22 | 23 | .. rubric:: Methods Details 24 | 25 | 26 | .. raw:: html 27 | 28 |
29 | 30 | .. autoclass:: SurfaceWidget 31 | :members: 32 | 33 | .. raw:: html 34 | 35 |
36 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # niwidgets documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 2 16:25:09 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # import re 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | 22 | # import os 23 | 24 | import sys 25 | 26 | if sys.platform == "darwin": # noqa 27 | import matplotlib # noqa 28 | 29 | matplotlib.use("PS") # noqa 30 | 31 | 32 | # sys.path.insert(0, os.path.abspath('.')) 33 | import sphinx_rtd_theme 34 | from docutils.parsers.rst import directives 35 | 36 | # from sphinx.ext.autodoc import ClassDocumenter 37 | from sphinx.ext.autosummary import Autosummary, get_documenter 38 | from sphinx.util.inspect import safe_getattr 39 | import niwidgets 40 | 41 | # -- General configuration ------------------------------------------------ 42 | 43 | # If your documentation needs a minimal Sphinx version, state it here. 44 | # 45 | # needs_sphinx = '1.0' 46 | 47 | # Add any Sphinx extension module names here, as strings. They can be 48 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 49 | # ones. 50 | extensions = [ 51 | "sphinx.ext.autodoc", 52 | "sphinx.ext.todo", 53 | "sphinx.ext.coverage", 54 | "sphinx.ext.mathjax", 55 | "sphinx.ext.viewcode", 56 | "sphinx.ext.githubpages", 57 | "sphinx.ext.intersphinx", 58 | "nbsphinx", 59 | "sphinx.ext.napoleon", 60 | "sphinx.ext.autosummary", 61 | "jupyter_sphinx.embed_widgets", 62 | "m2r", 63 | ] 64 | 65 | 66 | # Add any paths that contain templates here, relative to this directory. 67 | templates_path = ["_templates"] 68 | nbsphinx_allow_errors = True 69 | # The suffix(es) of source filenames. 70 | # You can specify multiple suffix as a list of string: 71 | # 72 | source_suffix = [".rst", ".md"] 73 | # source_suffix = ".rst" 74 | 75 | # whether there will be files for each autosummarised doc 76 | autosummary_generate = True 77 | autoclass_content = "neither" 78 | # autodoc_default_flags = ['members'] 79 | 80 | # The master toctree document. 81 | master_doc = "index" 82 | 83 | # General information about the project. 84 | project = "niwidgets" 85 | copyright = "2017, Jan Freyberg" 86 | author = "Jan Freyberg" 87 | 88 | # The version info for the project you're documenting, acts as replacement for 89 | # |version| and |release|, also used in various other places throughout the 90 | # built documents. 91 | # 92 | release = version = niwidgets.__version__ 93 | 94 | # The language for content autogenerated by Sphinx. Refer to documentation 95 | # for a list of supported languages. 96 | # 97 | # This is also used if you do content translation via gettext catalogs. 98 | # Usually you set "language" from the command line for these cases. 99 | language = None 100 | 101 | # List of patterns, relative to source directory, that match files and 102 | # directories to ignore when looking for source files. 103 | # This patterns also effect to html_static_path and html_extra_path 104 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".ipynb_checkpoints"] 105 | 106 | # The name of the Pygments (syntax highlighting) style to use. 107 | pygments_style = "sphinx" 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = True 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | 118 | html_logo = "logo.png" 119 | 120 | # Activate the theme. 121 | html_theme = "sphinx_rtd_theme" 122 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | # 128 | # html_theme_options = {"bootswatch_theme": "simplex"} 129 | 130 | # Add any paths that contain custom static files (such as style sheets) here, 131 | # relative to this directory. They are copied after the builtin static files, 132 | # so a file named "default.css" will overwrite the builtin "default.css". 133 | html_static_path = ["_static"] 134 | 135 | 136 | # -- Options for HTMLHelp output ------------------------------------------ 137 | 138 | # Output file base name for HTML help builder. 139 | htmlhelp_basename = "niwidgetsdoc" 140 | 141 | 142 | # -- Options for LaTeX output --------------------------------------------- 143 | 144 | latex_elements = { 145 | # The paper size ('letterpaper' or 'a4paper'). 146 | # 147 | # 'papersize': 'letterpaper', 148 | # The font size ('10pt', '11pt' or '12pt'). 149 | # 150 | # 'pointsize': '10pt', 151 | # Additional stuff for the LaTeX preamble. 152 | # 153 | # 'preamble': '', 154 | # Latex figure (float) alignment 155 | # 156 | # 'figure_align': 'htbp', 157 | } 158 | 159 | # Grouping the document tree into LaTeX files. List of tuples 160 | # (source start file, target name, title, 161 | # author, documentclass [howto, manual, or own class]). 162 | latex_documents = [ 163 | ( 164 | master_doc, 165 | "niwidgets.tex", 166 | "niwidgets Documentation", 167 | "Jan Freyberg", 168 | "manual", 169 | ) 170 | ] 171 | 172 | 173 | # -- Options for manual page output --------------------------------------- 174 | 175 | # One entry per manual page. List of tuples 176 | # (source start file, name, description, authors, manual section). 177 | man_pages = [(master_doc, "niwidgets", "niwidgets Documentation", [author], 1)] 178 | 179 | 180 | # -- Options for Texinfo output ------------------------------------------- 181 | 182 | # Grouping the document tree into Texinfo files. List of tuples 183 | # (source start file, target name, title, author, 184 | # dir menu entry, description, category) 185 | texinfo_documents = [ 186 | ( 187 | master_doc, 188 | "niwidgets", 189 | "niwidgets Documentation", 190 | author, 191 | "niwidgets", 192 | "One line description of project.", 193 | "Miscellaneous", 194 | ) 195 | ] 196 | 197 | 198 | # -- Options for intersphinx mappint -------------------------------------- 199 | 200 | intersphinx_mapping = { 201 | "python": ("https://docs.python.org/3.6", None), 202 | "mne": ("https://mne-tools.github.io/stable", None), 203 | "np": ("https://docs.scipy.org/doc/numpy-1.10.0/", None), 204 | } 205 | 206 | 207 | # -- Options for Epub output ---------------------------------------------- 208 | 209 | # Bibliographic Dublin Core info. 210 | epub_title = project 211 | epub_author = author 212 | epub_publisher = author 213 | epub_copyright = copyright 214 | 215 | # The unique identifier of the text. This can be a ISBN number 216 | # or the project homepage. 217 | # 218 | # epub_identifier = '' 219 | 220 | # A unique identification for the text. 221 | # 222 | # epub_uid = '' 223 | 224 | # A list of files that should not be packed into the epub file. 225 | epub_exclude_files = ["search.html"] 226 | 227 | 228 | # -- Extend autosummary --------------------------------------------------- 229 | 230 | 231 | class AutoAutoSummary(Autosummary): 232 | 233 | option_spec = { 234 | "methods": directives.unchanged, 235 | "attributes": directives.unchanged, 236 | } 237 | 238 | required_arguments = 1 239 | 240 | @staticmethod 241 | def get_members(obj, typ, include_public=None): 242 | if not include_public: 243 | include_public = [] 244 | items = [] 245 | for name in dir(obj): 246 | try: 247 | documenter = get_documenter(safe_getattr(obj, name), obj) 248 | except AttributeError: 249 | continue 250 | if documenter.objtype == typ: 251 | items.append(name) 252 | public = [ 253 | x for x in items if x in include_public or not x.startswith("_") 254 | ] 255 | return public, items 256 | 257 | def run(self): 258 | clazz = self.arguments[0] 259 | try: 260 | (module_name, class_name) = clazz.rsplit(".", 1) 261 | if "." in class_name: 262 | (class_name, method_name) = class_name.rsplit(".", 1) 263 | else: 264 | method_name = None 265 | m = __import__(module_name, globals(), locals(), [class_name]) 266 | c = getattr(m, class_name) 267 | if "methods" in self.options: 268 | _, methods = self.get_members(c, "method", ["__init__"]) 269 | _, supmethods = self.get_members( 270 | super(c), "method", ["__init__"] 271 | ) 272 | 273 | self.content = [ 274 | "~%s.%s" % (clazz, method) 275 | for method in methods 276 | if not method.startswith("_") and method in c.__dict__ 277 | ] 278 | if "attributes" in self.options: 279 | _, attribs = self.get_members(c, "attribute") 280 | self.content = [ 281 | "~%s.%s" % (clazz, attrib) 282 | for attrib in attribs 283 | if not attrib.startswith("_") 284 | ] 285 | finally: 286 | return super(AutoAutoSummary, self).run() 287 | 288 | 289 | def setup(app): 290 | app.add_stylesheet("css/custom.css") 291 | app.add_directive("autoautosummary", AutoAutoSummary) 292 | # app.add_autodocumenter(CleanClassDocumenter) 293 | -------------------------------------------------------------------------------- /docs/img/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/docs/img/example.gif -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | .. niwidgets documentation master file, created by 2 | 3 | # Make your neuroimaging plots interactive. 4 | 5 | `niwidgets` is a package that provides easy and general wrappers to display interactive widgets that visualise standard-format neuroimaging data, using new functions and standard functions from other libraries. 6 | 7 | ![](img/example.gif) 8 | 9 | `niwidgets` was initially developed by [Bjoern Soergel](http://www.ast.cam.ac.uk/~bs538/index.html) and [Jan Freyberg](http://www.janfreyberg.com/). 10 | 11 | It's actively being developed by members of the 12 | [brainhack](http://www.brainhack.org/) community, in particular 13 | [Satrajit Ghosh](https://github.com/satra), 14 | [Melanie Ganz](https://github.com/melanieganz), 15 | [Murat Bilgel](https://github.com/bilgelm), 16 | [Ariel Rokem](https://github.com/arokem), 17 | and [@elyb01](https://github.com/elyb01). 18 | 19 | We welcome contributions of any kind - feature suggestions, feature additions or bug reports should all be done at http://www.github.com/nipy/niwidgets. 20 | 21 | .. toctree:: 22 | :maxdepth: 3 23 | :caption: Documentation content: 24 | 25 | installation.rst 26 | examples.ipynb 27 | api 28 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install via ``pip`` 5 | ------------------- 6 | 7 | Install latest stable from pypi via::: 8 | 9 | pip install niwidgets 10 | 11 | Install the latest development version::: 12 | 13 | pip install git+git://github.com/janfreyberg/niwidgets/ 14 | 15 | Dependencies 16 | ------------ 17 | 18 | When you install niwidgets via `pip`, it will automatically install the packages it depends on. However, you will have to make sure that they are enabled as jupyter extensions. 19 | 20 | In particular, you will need to run the following two commands::: 21 | 22 | jupyter nbextension enable --py widgetsnbextension 23 | jupyter nbextension enable --py --sys-prefix ipyvolume 24 | 25 | This enables widgets in your jupyter notebook application, and also enables ipyvolume (which is used to display surface widgets). It's recommended to run these two commands if you're having the issue that rather than displaying a widget, your code produces text saying ``A Jupyter widget.`` 26 | 27 | It should be noted that at the moment, this doesn't work in jupyterlab, as widget support in jupyterlab is at very early stages. 28 | 29 | Development installation 30 | ------------------------ 31 | 32 | As always with pip packages, you can install a "development" version of this package by cloning the git repository and installing it via::: 33 | 34 | pip install -e /path/to/package 35 | 36 | This means you can make changes in your code locally and they will affect your code straight away. To make this even easier, you can add the following two lines to the top of your notebook, which ensure packages are re-loaded every time you run code (so you don't have to restart your jupyter kernel each time)::: 37 | 38 | %load_ext autoreload 39 | %autoreload 2 40 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/docs/logo.png -------------------------------------------------------------------------------- /docs/old-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/docs/old-logo.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.12 2 | appnope==0.1.0 3 | atomicwrites==1.3.0 4 | attrs==19.1.0 5 | babel==2.6.0 6 | backcall==0.1.0 7 | bleach==3.1.1 8 | certifi==2019.3.9 9 | chardet==3.0.4 10 | colorama==0.4.1 11 | coverage==4.5.3 12 | coveralls==1.7.0 13 | cycler==0.10.0 14 | decorator==4.4.0 15 | defusedxml==0.6.0 16 | docopt==0.6.2 17 | docutils==0.14 18 | entrypoints==0.3 19 | idna==2.8 20 | imagesize==1.1.0 21 | ipydatawidgets==4.0.0 22 | ipykernel==5.1.0 23 | ipython==7.4.0 24 | ipython-genutils==0.2.0 25 | ipyvolume==0.5.1 26 | ipywebrtc==0.4.3 27 | ipywidgets==7.4.2 28 | jedi==0.13.3 29 | jinja2==2.10.1 30 | jsonschema==3.0.1 31 | jupyter-client==5.2.4 32 | jupyter-core==4.4.0 33 | jupyter-sphinx==0.1.4 34 | jupyterlab==0.35.4 35 | jupyterlab-server==0.2.0 36 | kiwisolver==1.0.1 37 | m2r==0.2.1 38 | markupsafe==1.1.1 39 | matplotlib==3.0.3 40 | mistune==0.8.4 41 | more-itertools==7.0.0 42 | nbconvert==5.4.1 43 | nbformat==4.4.0 44 | nbsphinx==0.4.2 45 | nibabel==2.4.0 46 | nilearn==0.5.2 47 | notebook==5.7.8 48 | numpy==1.16.3 49 | packaging==19.0 50 | pandocfilters==1.4.2 51 | parso==0.4.0 52 | pathlib2==2.3.3 53 | pexpect==4.7.0 54 | pickleshare==0.7.5 55 | pillow==6.0.0 56 | pluggy==0.9.0 57 | prometheus-client==0.6.0 58 | prompt-toolkit==2.0.9 59 | ptyprocess==0.6.0 60 | py==1.8.0 61 | pygments==2.3.1 62 | pyparsing==2.4.0 63 | pyrsistent==0.14.11 64 | pytest==4.4.1 65 | pytest-cov==2.6.1 66 | python-dateutil==2.8.0 67 | pythreejs==2.0.2 68 | pytz==2019.1 69 | pywinpty==0.5.5 70 | pyzmq==18.0.1 71 | requests==2.21.0 72 | scikit-learn==0.20.3 73 | scipy==1.2.1 74 | send2trash==1.5.0 75 | six==1.12.0 76 | snowballstemmer==1.2.1 77 | sphinx==2.0.1 78 | sphinx-rtd-theme==0.4.3 79 | sphinxcontrib-applehelp==1.0.1 80 | sphinxcontrib-devhelp==1.0.1 81 | sphinxcontrib-htmlhelp==1.0.2 82 | sphinxcontrib-jsmath==1.0.1 83 | sphinxcontrib-qthelp==1.0.2 84 | sphinxcontrib-serializinghtml==1.1.3 85 | terminado==0.8.2 86 | testpath==0.4.2 87 | tornado==6.0.2 88 | traitlets==4.3.2 89 | traittypes==0.2.1 90 | urllib3==1.24.2 91 | wcwidth==0.1.7 92 | webencodings==0.5.1 93 | widgetsnbextension==3.4.2 94 | win-unicode-console==0.5 95 | -------------------------------------------------------------------------------- /index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Usage: `niwidgets` examples\n", 8 | "\n", 9 | "An online interactive version of this notebook can be found on [Binder](https://mybinder.org/v2/gh/nipy/niwidgets/master?filepath=index.ipynb).\n" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# Volume data\n", 17 | "\n", 18 | "The class for volume images is `NiftiWidget`. It takes a path to a `.nii` file as input." 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "## Interactively plotting an image\n", 26 | "\n", 27 | "We'll start by demonstrating the most useful aspect of the package: Being able to interactively slice an image file. To do so, we will import one of the example files that ships with `niwidgets`, a T1 weighted structural scan." 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 2, 33 | "metadata": {}, 34 | "outputs": [ 35 | { 36 | "data": { 37 | "text/plain": [ 38 | "" 39 | ] 40 | }, 41 | "metadata": {}, 42 | "output_type": "display_data" 43 | }, 44 | { 45 | "data": { 46 | "application/vnd.jupyter.widget-view+json": { 47 | "model_id": "ac2e8e975d9549ef8a92e57aa05904a2", 48 | "version_major": 2, 49 | "version_minor": 0 50 | }, 51 | "text/html": [ 52 | "

Failed to display Jupyter Widget of type interactive.

\n", 53 | "

\n", 54 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 55 | " that the widgets JavaScript is still loading. If this message persists, it\n", 56 | " likely means that the widgets JavaScript library is either not installed or\n", 57 | " not enabled. See the Jupyter\n", 58 | " Widgets Documentation for setup instructions.\n", 59 | "

\n", 60 | "

\n", 61 | " If you're reading this message in another frontend (for example, a static\n", 62 | " rendering on GitHub or NBViewer),\n", 63 | " it may mean that your frontend doesn't currently support widgets.\n", 64 | "

\n" 65 | ], 66 | "text/plain": [ 67 | "interactive(children=(IntSlider(value=45, continuous_update=False, description='x', max=90), IntSlider(value=54, continuous_update=False, description='y', max=108), IntSlider(value=45, continuous_update=False, description='z', max=90), Dropdown(description='Colormap:', index=73, options=('Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'), value='summer'), Output()), _dom_classes=('widget-interact',))" 68 | ] 69 | }, 70 | "metadata": {}, 71 | "output_type": "display_data" 72 | } 73 | ], 74 | "source": [ 75 | "# Let's try a simple parcellation map from a standard atlas\n", 76 | "from niwidgets import NiftiWidget\n", 77 | "from niwidgets import examplet1\n", 78 | "\n", 79 | "test_widget = NiftiWidget(examplet1)\n", 80 | "test_widget.nifti_plotter()" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "## Use plotting functions from `nilearn`\n", 88 | "\n", 89 | "`niwidgets` also lets you turn standard plots from the nilearn package into widgets. You can use any of them, and provide your own keyword arguments to set the slider options (if no key word argument is provided defaults are used).\n", 90 | "\n", 91 | "In particular, `niwidgets` should allow you to pick a colormap interactively for almost any plot type\n", 92 | "\n", 93 | "### `plot_epi`\n", 94 | "\n", 95 | "Plotting the same image but with the `nilearn` function `plot_epi`, we get this:\n" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 4, 101 | "metadata": { 102 | "scrolled": false 103 | }, 104 | "outputs": [ 105 | { 106 | "data": { 107 | "text/plain": [ 108 | "" 109 | ] 110 | }, 111 | "metadata": {}, 112 | "output_type": "display_data" 113 | }, 114 | { 115 | "data": { 116 | "application/vnd.jupyter.widget-view+json": { 117 | "model_id": "064b056b04d8405cb66ea2d4d4ae7395", 118 | "version_major": 2, 119 | "version_minor": 0 120 | }, 121 | "text/html": [ 122 | "

Failed to display Jupyter Widget of type interactive.

\n", 123 | "

\n", 124 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 125 | " that the widgets JavaScript is still loading. If this message persists, it\n", 126 | " likely means that the widgets JavaScript library is either not installed or\n", 127 | " not enabled. See the Jupyter\n", 128 | " Widgets Documentation for setup instructions.\n", 129 | "

\n", 130 | "

\n", 131 | " If you're reading this message in another frontend (for example, a static\n", 132 | " rendering on GitHub or NBViewer),\n", 133 | " it may mean that your frontend doesn't currently support widgets.\n", 134 | "

\n" 135 | ], 136 | "text/plain": [ 137 | "interactive(children=(Dropdown(description='display_mode', options=('ortho', 'x', 'y', 'z', 'yx', 'xz', 'yz'), value='ortho'), Dropdown(description='Colormap:', index=73, options=('Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'), value='summer'), IntSlider(value=0, continuous_update=False, description='x', max=90, min=-90), IntSlider(value=0, continuous_update=False, description='y', max=90, min=-90), IntSlider(value=0, continuous_update=False, description='z', max=90, min=-90), Output()), _dom_classes=('widget-interact',))" 138 | ] 139 | }, 140 | "metadata": {}, 141 | "output_type": "display_data" 142 | } 143 | ], 144 | "source": [ 145 | "import nilearn.plotting as nip\n", 146 | "\n", 147 | "test_widget.nifti_plotter(plotting_func=nip.plot_epi, display_mode=['ortho', 'x', 'y', 'z', 'yx', 'xz', 'yz'])" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "### `plot_glass_brain`\n", 155 | "\n", 156 | "This is an example of a glass brain plot with a standard visual perception activation map from neurosynth (this also ships as an example with `niwidgets`:" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 5, 162 | "metadata": {}, 163 | "outputs": [ 164 | { 165 | "data": { 166 | "text/plain": [ 167 | "" 168 | ] 169 | }, 170 | "metadata": {}, 171 | "output_type": "display_data" 172 | }, 173 | { 174 | "data": { 175 | "application/vnd.jupyter.widget-view+json": { 176 | "model_id": "8f5041a516c04abbb045303a0bcd48f5", 177 | "version_major": 2, 178 | "version_minor": 0 179 | }, 180 | "text/html": [ 181 | "

Failed to display Jupyter Widget of type interactive.

\n", 182 | "

\n", 183 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 184 | " that the widgets JavaScript is still loading. If this message persists, it\n", 185 | " likely means that the widgets JavaScript library is either not installed or\n", 186 | " not enabled. See the Jupyter\n", 187 | " Widgets Documentation for setup instructions.\n", 188 | "

\n", 189 | "

\n", 190 | " If you're reading this message in another frontend (for example, a static\n", 191 | " rendering on GitHub or NBViewer),\n", 192 | " it may mean that your frontend doesn't currently support widgets.\n", 193 | "

\n" 194 | ], 195 | "text/plain": [ 196 | "interactive(children=(FloatSlider(value=5.0, description='threshold', max=10.0, step=0.01), Dropdown(description='display_mode', options=('ortho', 'xz'), value='ortho'), Dropdown(description='Colormap:', index=73, options=('Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'), value='summer'), Output()), _dom_classes=('widget-interact',))" 197 | ] 198 | }, 199 | "metadata": {}, 200 | "output_type": "display_data" 201 | } 202 | ], 203 | "source": [ 204 | "from niwidgets import examplezmap\n", 205 | "import nilearn.plotting as nip\n", 206 | "test = NiftiWidget(examplezmap)\n", 207 | "test.nifti_plotter(plotting_func=nip.plot_glass_brain, threshold=(0.0, 10.0, 0.01),\n", 208 | " display_mode=['ortho','xz'])" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": {}, 214 | "source": [ 215 | "### `plot_img`\n", 216 | "\n", 217 | "Another image slicer type plot from the nilearn package, this time with an example atlas (the CC400 atlas), and setting the colormap:" 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 6, 223 | "metadata": {}, 224 | "outputs": [ 225 | { 226 | "data": { 227 | "text/plain": [ 228 | "" 229 | ] 230 | }, 231 | "metadata": {}, 232 | "output_type": "display_data" 233 | }, 234 | { 235 | "data": { 236 | "application/vnd.jupyter.widget-view+json": { 237 | "model_id": "8b8c4fa8e6264dfb9d69b1d4c9a22f26", 238 | "version_major": 2, 239 | "version_minor": 0 240 | }, 241 | "text/html": [ 242 | "

Failed to display Jupyter Widget of type interactive.

\n", 243 | "

\n", 244 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 245 | " that the widgets JavaScript is still loading. If this message persists, it\n", 246 | " likely means that the widgets JavaScript library is either not installed or\n", 247 | " not enabled. See the Jupyter\n", 248 | " Widgets Documentation for setup instructions.\n", 249 | "

\n", 250 | "

\n", 251 | " If you're reading this message in another frontend (for example, a static\n", 252 | " rendering on GitHub or NBViewer),\n", 253 | " it may mean that your frontend doesn't currently support widgets.\n", 254 | "

\n" 255 | ], 256 | "text/plain": [ 257 | "interactive(children=(Dropdown(description='display_mode', options=('ortho', 'x', 'y', 'z'), value='ortho'), IntSlider(value=0, continuous_update=False, description='x', max=90, min=-90), IntSlider(value=0, continuous_update=False, description='y', max=90, min=-90), IntSlider(value=0, continuous_update=False, description='z', max=90, min=-90), Output()), _dom_classes=('widget-interact',))" 258 | ] 259 | }, 260 | "metadata": {}, 261 | "output_type": "display_data" 262 | } 263 | ], 264 | "source": [ 265 | "from niwidgets import NiftiWidget\n", 266 | "from niwidgets import exampleatlas\n", 267 | "atlas_widget = NiftiWidget(exampleatlas)\n", 268 | "atlas_widget.nifti_plotter(plotting_func=nip.plot_img, display_mode=['ortho', 'x', 'y', 'z'], colormap='hot')" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "metadata": {}, 274 | "source": [ 275 | "# Surface data\n", 276 | "\n", 277 | "If you have surface data, you can import the `SurfaceWidget` and use it in a similar fashion to the `NiftiWidget`. Import and define it in the same way:" 278 | ] 279 | }, 280 | { 281 | "cell_type": "code", 282 | "execution_count": 7, 283 | "metadata": {}, 284 | "outputs": [ 285 | { 286 | "data": { 287 | "application/vnd.jupyter.widget-view+json": { 288 | "model_id": "51cfd92f17df4fc786205ee1beae45d3", 289 | "version_major": 2, 290 | "version_minor": 0 291 | }, 292 | "text/html": [ 293 | "

Failed to display Jupyter Widget of type interactive.

\n", 294 | "

\n", 295 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 296 | " that the widgets JavaScript is still loading. If this message persists, it\n", 297 | " likely means that the widgets JavaScript library is either not installed or\n", 298 | " not enabled. See the Jupyter\n", 299 | " Widgets Documentation for setup instructions.\n", 300 | "

\n", 301 | "

\n", 302 | " If you're reading this message in another frontend (for example, a static\n", 303 | " rendering on GitHub or NBViewer),\n", 304 | " it may mean that your frontend doesn't currently support widgets.\n", 305 | "

\n" 306 | ], 307 | "text/plain": [ 308 | "interactive(children=(Dropdown(description='Colormap:', index=73, options=('Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'), value='summer'), Output()), _dom_classes=('widget-interact',))" 309 | ] 310 | }, 311 | "metadata": {}, 312 | "output_type": "display_data" 313 | }, 314 | { 315 | "data": { 316 | "application/vnd.jupyter.widget-view+json": { 317 | "model_id": "2624832a01314342a1366587a73d2508", 318 | "version_major": 2, 319 | "version_minor": 0 320 | }, 321 | "text/html": [ 322 | "

Failed to display Jupyter Widget of type Figure.

\n", 323 | "

\n", 324 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 325 | " that the widgets JavaScript is still loading. If this message persists, it\n", 326 | " likely means that the widgets JavaScript library is either not installed or\n", 327 | " not enabled. See the Jupyter\n", 328 | " Widgets Documentation for setup instructions.\n", 329 | "

\n", 330 | "

\n", 331 | " If you're reading this message in another frontend (for example, a static\n", 332 | " rendering on GitHub or NBViewer),\n", 333 | " it may mean that your frontend doesn't currently support widgets.\n", 334 | "

\n" 335 | ], 336 | "text/plain": [ 337 | "Figure(camera_center=[0.0, 0.0, 0.0], camera_fov=1.0, height=600, matrix_projection=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], matrix_world=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], meshes=[Mesh(color=array([[1., 1., 1.],\n", 338 | " [1., 1., 1.],\n", 339 | " [1., 1., 1.],\n", 340 | " ...,\n", 341 | " [1., 1., 1.],\n", 342 | " [1., 1., 1.],\n", 343 | " [1., 1., 1.]]), texture=None, triangles=array([[ 0, 1, 3],\n", 344 | " [ 4, 3, 1],\n", 345 | " [ 0, 91, 1],\n", 346 | " ...,\n", 347 | " [123307, 123906, 123895],\n", 348 | " [123906, 123907, 123895],\n", 349 | " [123885, 124986, 124987]], dtype=uint32), x=array([18.7413578 , 18.37849426, 19.00789833, ..., 17.78539658,\n", 350 | " 17.8218174 , 17.95105362]), y=array([-127.09669495, -127.08947754, -127.14855194, ..., 96.06409454,\n", 351 | " 96.46263123, 96.3392868 ]), z=array([-48.42454529, -48.42292786, -48.9959259 , ..., 44.24810028,\n", 352 | " 44.08300781, 44.18468475]))], style={'axes': {'color': 'black', 'label': {'color': 'black'}, 'ticklabel': {'color': 'black'}, 'visible': False}, 'background-color': 'white', 'box': {'visible': False}}, tf=None, width=600, xlim=[-100.0, 100.0], ylim=[-127.3917007446289, 127.3917007446289], zlim=[-100.0, 100.0])" 353 | ] 354 | }, 355 | "metadata": {}, 356 | "output_type": "display_data" 357 | } 358 | ], 359 | "source": [ 360 | "from niwidgets import SurfaceWidget\n", 361 | "from niwidgets.exampledata import examplesurface\n", 362 | "\n", 363 | "surface_widget = SurfaceWidget(examplesurface)\n", 364 | "surface_widget.surface_plotter()" 365 | ] 366 | }, 367 | { 368 | "cell_type": "markdown", 369 | "metadata": {}, 370 | "source": [ 371 | "If you want to plot additional data as overlays, you can pass those as either loaded `GiftiImages` (loaded using nibabel), or as file paths to a `.annot`, `.thickness`, `.curv`, `.sulc`, or `.gii` file.\n", 372 | "\n", 373 | "If you pass them in a dictionary (see e.g. `niwidgets.exampledata.exampleoverlays`), the keys of the dictionary are used for the Options in a dropdown menu:" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": 8, 379 | "metadata": { 380 | "scrolled": false 381 | }, 382 | "outputs": [ 383 | { 384 | "data": { 385 | "application/vnd.jupyter.widget-view+json": { 386 | "model_id": "6b090722cb50426fbaaecce2a3da82f3", 387 | "version_major": 2, 388 | "version_minor": 0 389 | }, 390 | "text/html": [ 391 | "

Failed to display Jupyter Widget of type interactive.

\n", 392 | "

\n", 393 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 394 | " that the widgets JavaScript is still loading. If this message persists, it\n", 395 | " likely means that the widgets JavaScript library is either not installed or\n", 396 | " not enabled. See the Jupyter\n", 397 | " Widgets Documentation for setup instructions.\n", 398 | "

\n", 399 | "

\n", 400 | " If you're reading this message in another frontend (for example, a static\n", 401 | " rendering on GitHub or NBViewer),\n", 402 | " it may mean that your frontend doesn't currently support widgets.\n", 403 | "

\n" 404 | ], 405 | "text/plain": [ 406 | "interactive(children=(Dropdown(description='Overlay:', options=('Area', 'Curvature', 'Thickness', 'Annotation'), value='Area'), Dropdown(description='Colormap:', index=73, options=('Accent', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'Dark2', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'Paired', 'Pastel1', 'Pastel2', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Set1', 'Set2', 'Set3', 'Spectral', 'Vega10', 'Vega20', 'Vega20b', 'Vega20c', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spectral', 'spring', 'summer', 'tab10', 'tab20', 'tab20b', 'tab20c', 'terrain', 'winter'), value='summer'), Output()), _dom_classes=('widget-interact',))" 407 | ] 408 | }, 409 | "metadata": {}, 410 | "output_type": "display_data" 411 | }, 412 | { 413 | "data": { 414 | "application/vnd.jupyter.widget-view+json": { 415 | "model_id": "3a05401222af4d7cbd4c2a98d4c1caf3", 416 | "version_major": 2, 417 | "version_minor": 0 418 | }, 419 | "text/html": [ 420 | "

Failed to display Jupyter Widget of type Figure.

\n", 421 | "

\n", 422 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 423 | " that the widgets JavaScript is still loading. If this message persists, it\n", 424 | " likely means that the widgets JavaScript library is either not installed or\n", 425 | " not enabled. See the Jupyter\n", 426 | " Widgets Documentation for setup instructions.\n", 427 | "

\n", 428 | "

\n", 429 | " If you're reading this message in another frontend (for example, a static\n", 430 | " rendering on GitHub or NBViewer),\n", 431 | " it may mean that your frontend doesn't currently support widgets.\n", 432 | "

\n" 433 | ], 434 | "text/plain": [ 435 | "Figure(camera_center=[0.0, 0.0, 0.0], camera_fov=1.0, height=600, matrix_projection=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], matrix_world=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], meshes=[Mesh(color=array([[1., 1., 1.],\n", 436 | " [1., 1., 1.],\n", 437 | " [1., 1., 1.],\n", 438 | " ...,\n", 439 | " [1., 1., 1.],\n", 440 | " [1., 1., 1.],\n", 441 | " [1., 1., 1.]]), texture=None, triangles=array([[ 0, 1, 3],\n", 442 | " [ 4, 3, 1],\n", 443 | " [ 0, 91, 1],\n", 444 | " ...,\n", 445 | " [123307, 123906, 123895],\n", 446 | " [123906, 123907, 123895],\n", 447 | " [123885, 124986, 124987]], dtype=uint32), x=array([18.7413578 , 18.37849426, 19.00789833, ..., 17.78539658,\n", 448 | " 17.8218174 , 17.95105362]), y=array([-127.09669495, -127.08947754, -127.14855194, ..., 96.06409454,\n", 449 | " 96.46263123, 96.3392868 ]), z=array([-48.42454529, -48.42292786, -48.9959259 , ..., 44.24810028,\n", 450 | " 44.08300781, 44.18468475]))], style={'axes': {'color': 'black', 'label': {'color': 'black'}, 'ticklabel': {'color': 'black'}, 'visible': False}, 'background-color': 'white', 'box': {'visible': False}}, tf=None, width=600, xlim=[-100.0, 100.0], ylim=[-127.3917007446289, 127.3917007446289], zlim=[-100.0, 100.0])" 451 | ] 452 | }, 453 | "metadata": {}, 454 | "output_type": "display_data" 455 | } 456 | ], 457 | "source": [ 458 | "from niwidgets import SurfaceWidget\n", 459 | "from niwidgets.exampledata import examplesurface\n", 460 | "from niwidgets.exampledata import exampleoverlays\n", 461 | "\n", 462 | "surface_widget = SurfaceWidget(examplesurface, overlayfiles=exampleoverlays)\n", 463 | "\n", 464 | "surface_widget.surface_plotter()" 465 | ] 466 | }, 467 | { 468 | "cell_type": "markdown", 469 | "metadata": {}, 470 | "source": [ 471 | "## Streamlines\n", 472 | "\n", 473 | "If you have mrtrix or trackvis streamlines, you can display them using the `StreamlineWidget`. Instead of passing a streamlines file, one can also pass a nibabel streamline sequence to the widget." 474 | ] 475 | }, 476 | { 477 | "cell_type": "code", 478 | "execution_count": 9, 479 | "metadata": {}, 480 | "outputs": [ 481 | { 482 | "data": { 483 | "application/vnd.jupyter.widget-view+json": { 484 | "model_id": "de66333d4cce48418133e6ef22f90f5a", 485 | "version_major": 2, 486 | "version_minor": 0 487 | }, 488 | "text/html": [ 489 | "

Failed to display Jupyter Widget of type VBox.

\n", 490 | "

\n", 491 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 492 | " that the widgets JavaScript is still loading. If this message persists, it\n", 493 | " likely means that the widgets JavaScript library is either not installed or\n", 494 | " not enabled. See the Jupyter\n", 495 | " Widgets Documentation for setup instructions.\n", 496 | "

\n", 497 | "

\n", 498 | " If you're reading this message in another frontend (for example, a static\n", 499 | " rendering on GitHub or NBViewer),\n", 500 | " it may mean that your frontend doesn't currently support widgets.\n", 501 | "

\n" 502 | ], 503 | "text/plain": [ 504 | "VBox(children=(Figure(camera_center=[0.0, 0.0, 0.0], camera_fov=1.0, height=500, matrix_projection=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], matrix_world=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], meshes=[Mesh(color=array([[ 0.5103893 , 0.7629744 , -0.3967025 ],\n", 505 | " [ 0.5103893 , 0.7629744 , -0.3967025 ],\n", 506 | " [ 0.5103893 , 0.7629744 , -0.3967025 ],\n", 507 | " ...,\n", 508 | " [-0.06500284, -0.4601881 , 0.8854386 ],\n", 509 | " [-0.06500284, -0.4601881 , 0.8854386 ],\n", 510 | " [-0.06500284, -0.4601881 , 0.8854386 ]], dtype=float32), lines=array([ 0, 1, 1, ..., 161141, 161141, 161142], dtype=uint32), texture=None, x=array([76.33768 , 76.1091 , 75.86626 , ..., 49.14276 , 49.224125,\n", 511 | " 49.276688], dtype=float32), y=array([49.301598, 48.87002 , 48.451088, ..., 45.552483, 45.69906 ,\n", 512 | " 45.840733], dtype=float32), z=array([28.900988 , 29.0082 , 29.132801 , ..., 0.87795854,\n", 513 | " 0.40690082, -0.0697186 ], dtype=float32))], style={'axes': {'color': 'red', 'label': {'color': 'white'}, 'ticklabel': {'color': 'white'}, 'visible': False}, 'background-color': 'white', 'box': {'visible': False}}, tf=None, xlim=[-0.497088223695755, 94.8163833618164], ylim=[-0.497088223695755, 94.8163833618164], zlim=[-0.497088223695755, 94.8163833618164]),))" 514 | ] 515 | }, 516 | "metadata": {}, 517 | "output_type": "display_data" 518 | }, 519 | { 520 | "data": { 521 | "application/vnd.jupyter.widget-view+json": { 522 | "model_id": "ccdeeb65175d403491649ee1a498a14e", 523 | "version_major": 2, 524 | "version_minor": 0 525 | }, 526 | "text/html": [ 527 | "

Failed to display Jupyter Widget of type interactive.

\n", 528 | "

\n", 529 | " If you're reading this message in the Jupyter Notebook or JupyterLab Notebook, it may mean\n", 530 | " that the widgets JavaScript is still loading. If this message persists, it\n", 531 | " likely means that the widgets JavaScript library is either not installed or\n", 532 | " not enabled. See the Jupyter\n", 533 | " Widgets Documentation for setup instructions.\n", 534 | "

\n", 535 | "

\n", 536 | " If you're reading this message in another frontend (for example, a static\n", 537 | " rendering on GitHub or NBViewer),\n", 538 | " it may mean that your frontend doesn't currently support widgets.\n", 539 | "

\n" 540 | ], 541 | "text/plain": [ 542 | "interactive(children=(FloatSlider(value=43.99999313354492, continuous_update=False, description='threshold', max=97.0, min=13.999998092651367), Output()), _dom_classes=('widget-interact',))" 543 | ] 544 | }, 545 | "metadata": {}, 546 | "output_type": "display_data" 547 | } 548 | ], 549 | "source": [ 550 | "from niwidgets import StreamlineWidget\n", 551 | "from niwidgets.exampledata import streamlines\n", 552 | "\n", 553 | "sw = StreamlineWidget(filename=streamlines)\n", 554 | "style = {'axes': {'color': 'red',\n", 555 | " 'label': {'color': 'white'},\n", 556 | " 'ticklabel': {'color': 'white'},\n", 557 | " 'visible': False},\n", 558 | " 'background-color': 'white',\n", 559 | " 'box': {'visible': False}}\n", 560 | "sw.plot(display_fraction=0.5, width=500, height=500, style=style, percentile=80)" 561 | ] 562 | }, 563 | { 564 | "cell_type": "code", 565 | "execution_count": null, 566 | "metadata": {}, 567 | "outputs": [], 568 | "source": [] 569 | } 570 | ], 571 | "metadata": { 572 | "kernelspec": { 573 | "display_name": "Python [default]", 574 | "language": "python", 575 | "name": "python3" 576 | }, 577 | "language_info": { 578 | "codemirror_mode": { 579 | "name": "ipython", 580 | "version": 3 581 | }, 582 | "file_extension": ".py", 583 | "mimetype": "text/x-python", 584 | "name": "python", 585 | "nbconvert_exporter": "python", 586 | "pygments_lexer": "ipython3", 587 | "version": "3.6.4" 588 | } 589 | }, 590 | "nbformat": 4, 591 | "nbformat_minor": 2 592 | } 593 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "niwidgets" 3 | version = "0.2.2" 4 | description = "'Interactive jupyter widgets for neuroimaging.'" 5 | authors = ["Jan Freyberg ", "Bjoern Soergel"] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.5" 10 | ipywidgets = "^7.4" 11 | nibabel = "^2.4" 12 | ipyvolume = "^0.5.1" 13 | matplotlib = "^3.0" 14 | numpy = "^1.16" 15 | scipy = "^1.2" 16 | nilearn = "^0.5.2" 17 | scikit-learn = "^0.20.3" 18 | 19 | [tool.poetry.dev-dependencies] 20 | jupyterlab = "^0.35.4" 21 | pytest = "^4.4" 22 | sphinx = "^2.0" 23 | nbsphinx = "^0.4.2" 24 | jupyter_sphinx = "^0.1.4" 25 | m2r = "^0.2.1" 26 | pytest-cov = "^2.6" 27 | coveralls = "^1.7" 28 | sphinx_rtd_theme = "^0.4.3" 29 | 30 | [tool.black] 31 | line-length = 79 32 | [build-system] 33 | requires = ["poetry>=0.12"] 34 | build-backend = "poetry.masonry.api" 35 | 36 | -------------------------------------------------------------------------------- /report.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "deletable": true, 7 | "editable": true 8 | }, 9 | "source": [ 10 | "# Niwidgets: interactive visualisation of neuroimaging data" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": { 16 | "deletable": true, 17 | "editable": true 18 | }, 19 | "source": [ 20 | "## Abstract\n", 21 | "\n", 22 | "With a new python package, niwidgets, we attempt to make it easier to interactively visualise neuroimaging data in jupyter notebooks. Interactive visualisations are useful both for the research process, and for the final presentation of results. It takes away the pressure to produce one illustrative snapshot of your complex, multidimensional data, and instead allows the reader to investigate and explore the data themselves.\n", 23 | "\n", 24 | "This first release of niwidgets provides simple, one- or two-line implementations of interactive widgets in python. It provides interactive ways to slice a nifti file, and an interface to add custom interactive options such as thresholding a statistical map or changing the orientation of the plot. Combined with reports written in jupyter notebooks, this could enhance the write-up of neuroimaging studies by giving the reader the write-up, code, and interactive results all in one file." 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": { 30 | "deletable": true, 31 | "editable": true 32 | }, 33 | "source": [ 34 | "## Introduction\n", 35 | "\n", 36 | "Neuroimaging data is often highly complex. The results are often multidimensional and could be visualised in many different ways. The traditional model of presenting one snapshot of neuroimaging results only means that researchers face tough decisions on how to present their data, and can often lead to misleading figures in papers. One approach that has recently developed in the neuroimaging community is the publication of result maps - uploading the 3D results of an experiment to a repository so that readers of journal articles can investigate the results by themselves. One such example is neurovault.org Gorgolewski et al. 2015. These tools are extremely useful and provide online visualisation tools, which let readers explore the data they are reading about.\n", 37 | "\n", 38 | "However, this still divorces the data from the article itself, as well as the code that was used to analyse the data. We wanted to provide a way for neuroscientists to provide a coherent report that includes the article, analysis code, and data alongside each other. We chose to try to implement this for jupyter notebooks. Jupyter notebooks is a language-agnostic file format that combines rich text, code, and inline results and visualisations. Given that MATLAB, python, R and bash are all supported by jupyter notebooks, and that most jupyter notebook kernels support python, a python library that visualises brain data should allow researchers to produce nice interactive combinations of writing, code and data." 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": { 44 | "deletable": true, 45 | "editable": true 46 | }, 47 | "source": [ 48 | "## Approach\n", 49 | "\n", 50 | "We used the library ipywidgets as a basis and wrote wrapper functions that would handle the loading of neuroimaging data and the creation of interactive tools to let researchers manipulate data themselves. We used established neuroimaging packages for python - nibabel (Brett et al. 2016) and nilearn (Abraham et al. 2010) - to handle the loading of data, and used an established visualisation package for python - matplotlib - to handle the visualisation.\n", 51 | "\n", 52 | "We first wanted to provide a simple way to explore imaging maps, so we wrote a function that simply provided three sliders, for x, y and z, and allowed people to chose a color map.\n", 53 | "\n", 54 | "We then wanted to provide the ability to turn more sophisticated neuroimaging plots into widgets. To this end, we added the ability to provide a plotting function yourself. We so far only tested this with plotting functions from the package nilearn, as they are simple to use. When provided a custom plotting function, we tried to make niwidgets infer basic interactive features about the plot, such as whether it supports interactive x-, y-, and z-coordinates. We also tried to enable custom colormaps for all plots, which could be useful to prevent issues around categorical colormaps (Hawkins 2016) or colormaps unsuited for colorblind readers (Albrecht 2010)." 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": { 60 | "deletable": true, 61 | "editable": true 62 | }, 63 | "source": [ 64 | "## Results\n", 65 | "\n", 66 | "We produced a python package called niwidgets. So far, the package provides one class (NiWidget). To initialise this class, only one input is needed: the path to a nifti file.\n", 67 | "\n", 68 | "The NiWidget class provides one method at the moment (nifti_plotter). This method can be called without any input to provide a default function, and with a custom plotting function as input to provide more versatile plots." 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": { 75 | "collapsed": true, 76 | "deletable": true, 77 | "editable": true 78 | }, 79 | "outputs": [], 80 | "source": [ 81 | "from niwidgets import NiWidget" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": { 87 | "deletable": true, 88 | "editable": true 89 | }, 90 | "source": [ 91 | "### Default plotting function\n", 92 | "\n", 93 | "The default function produces a widget that allows the reader to choose any position within the image using three sliders (x, y and z). It also allows the reader to choose any of the colormaps that are part of the matplotlib package (Fig. 1). Creating this widget only requires two lines of code: 1) Initialise the class using NiWidget('/path/to/file') and 2) Create the widget using .nifti_plotter()" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": { 100 | "collapsed": false, 101 | "deletable": true, 102 | "editable": true 103 | }, 104 | "outputs": [], 105 | "source": [ 106 | "from niwidgets import examplet1 # this is an example T1 dataset\n", 107 | "my_widget = NiWidget(examplet1)\n", 108 | "my_widget.nifti_plotter()" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": { 114 | "deletable": true, 115 | "editable": true 116 | }, 117 | "source": [ 118 | "### Custom plotting functions\n", 119 | "\n", 120 | "When the nifti_plotter() method is called with the optional keyword argument plotting_func defined, it produces a plot that uses the provided function. We have tested this primarily with nilearn.plotting functions, but will test it for other functions in the future, too. We attempted to \"coerce\" these functions to support custom colormaps, and provide the same selection of colormaps we do for the default plotting function. We also tried to detect whether the plotting function supports the specification of x/y/z coordinates, and if so implement interactive sliders for them. An example of this is the nilearn.plotting.plot_glass_brain function (Fig. 2)." 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": { 127 | "collapsed": false, 128 | "deletable": true, 129 | "editable": true 130 | }, 131 | "outputs": [], 132 | "source": [ 133 | "from niwidgets import examplezmap # this is an example statistical map from neurosynth\n", 134 | "import nilearn.plotting as nip\n", 135 | "my_widget = NiWidget(examplezmap)\n", 136 | "my_widget.nifti_plotter(plotting_func=nip.plot_glass_brain, # custom plot function\n", 137 | " threshold=(0.0, 6.0, 0.01), # custom slider\n", 138 | " display_mode=['ortho','xz']) # custom drop-down menu" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": { 144 | "deletable": true, 145 | "editable": true 146 | }, 147 | "source": [ 148 | "## Limitations\n", 149 | "\n", 150 | "The most important limitation of niwidgets at the moment is that it only supports the illustration of volume imaging data in nifti files. In the future, we would like to also provide interactives for surface projections, such as those created with the python package pysurf.\n", 151 | "\n", 152 | "Additionally, for niwidgets to work, the reader has to run the notebook with their own installation of Ipython. While it is possible to do this remotely using the free service mybinder.org, it would be even better if widgets worked when notebooks are converted to static webpages. Notebooks are already viewable as static web pages on Github, a popular software repository, but dynamic widgets are currently not supported. However, this is in the process of being implemented by the ipywidget package maintainers, so this limitation may soon disappear.\n", 153 | "\n", 154 | "Lastly, the niwidgets package is not bug-free enough for a stable release yet. There are still many issues around implementing custom interactives with a diverse range of plotting functions (such as the nilearn.plotting.plot_stat_map function), and it will require more work to be stable. We encourage the opening of issues on the niwidgets github repository, as well as pull requests for the implementation of new features or fixing of bugs." 155 | ] 156 | } 157 | ], 158 | "metadata": { 159 | "celltoolbar": "Raw Cell Format", 160 | "kernelspec": { 161 | "display_name": "Python 3", 162 | "language": "python", 163 | "name": "python3" 164 | }, 165 | "language_info": { 166 | "codemirror_mode": { 167 | "name": "ipython", 168 | "version": 3 169 | }, 170 | "file_extension": ".py", 171 | "mimetype": "text/x-python", 172 | "name": "python", 173 | "nbconvert_exporter": "python", 174 | "pygments_lexer": "ipython3", 175 | "version": "3.6.0" 176 | } 177 | }, 178 | "nbformat": 4, 179 | "nbformat_minor": 2 180 | } 181 | -------------------------------------------------------------------------------- /src/niwidgets/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Widgets to visualise neuroimaging data. 3 | 4 | For volume images, try import NiftiWidget. 5 | For surface images, try SurfaceWidget. 6 | """ 7 | from .version import __version__ # noqa: F401 8 | from .exampledata import exampleatlas, examplezmap, examplet1 # noqa: F401 9 | from .niwidget_volume import NiftiWidget # noqa: F401 10 | from .niwidget_surface import SurfaceWidget # noqa: F401 11 | from .streamlines import StreamlineWidget # noqa: F401 12 | -------------------------------------------------------------------------------- /src/niwidgets/colormaps.py: -------------------------------------------------------------------------------- 1 | import ipywidgets 2 | 3 | # from matplotlib import pyplot as plt 4 | 5 | 6 | def get_cmap_dropdown(colormap): 7 | # set default colormap options & add them to the kwargs 8 | if colormap is None: 9 | # options = (sorted(m for m in plt.cm.datad if not m.endswith("_r"))) 10 | options = [ 11 | "viridis", 12 | "summer", 13 | "gray", 14 | "Blues", 15 | "Greens", 16 | "Greys", 17 | "Oranges", 18 | "Purples", 19 | "Reds", 20 | "nipy_spectral", 21 | ] 22 | return ipywidgets.Dropdown( 23 | options=options, 24 | value="viridis", 25 | description="Colormap:", 26 | indent=False, 27 | ) 28 | elif isinstance(colormap, (list, tuple)): 29 | return ipywidgets.Dropdown( 30 | options=colormap, value=colormap[0], description="Colormap:" 31 | ) 32 | elif isinstance(colormap, str): 33 | return ipywidgets.fixed(colormap) 34 | else: 35 | raise ValueError( 36 | "The colormap must be either a valid string, a list, " "or None." 37 | ) 38 | -------------------------------------------------------------------------------- /src/niwidgets/controls.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | import traitlets 3 | 4 | 5 | class PlaySlider(widgets.HBox): 6 | """ 7 | A combined Play / IntSlider widget. 8 | """ 9 | 10 | max = traitlets.Integer(100) 11 | min = traitlets.Integer(0) 12 | value = traitlets.Integer(0) 13 | step = traitlets.Integer(1) 14 | interval = traitlets.Integer(500) 15 | 16 | def __init__( 17 | self, 18 | *args, 19 | min=0, 20 | max=100, 21 | value=100, 22 | step=1, 23 | interval=500, 24 | label="Value", 25 | continuous_update=True, 26 | **kwargs 27 | ): 28 | 29 | # initialise the hbox widget 30 | super().__init__( 31 | [ 32 | widgets.Label(label + ": "), 33 | widgets.Play(min=min, max=max, value=value, interval=interval), 34 | widgets.IntSlider( 35 | min=min, 36 | max=max, 37 | value=value, 38 | step=step, 39 | continuous_update=continuous_update, 40 | ), 41 | ] 42 | ) 43 | for trait in ("min", "max", "value", "step"): 44 | # first the two control elements: 45 | widgets.link((self.children[1], trait), (self.children[2], trait)) 46 | # then link the latter with self: 47 | widgets.link((self.children[2], trait), (self, trait)) 48 | 49 | widgets.link((self.children[1], "interval"), (self, "interval")) 50 | -------------------------------------------------------------------------------- /src/niwidgets/data/T1.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/T1.nii.gz -------------------------------------------------------------------------------- /src/niwidgets/data/cc400_roi_atlas.nii: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/cc400_roi_atlas.nii -------------------------------------------------------------------------------- /src/niwidgets/data/cognitive control_pFgA_z.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/cognitive control_pFgA_z.nii.gz -------------------------------------------------------------------------------- /src/niwidgets/data/cognitive control_pFgA_z_FDR_0.01.nii.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/cognitive control_pFgA_z_FDR_0.01.nii.gz -------------------------------------------------------------------------------- /src/niwidgets/data/example_surfaces/aparc.annot.ctab: -------------------------------------------------------------------------------- 1 | 0 unknown 25 5 25 0 2 | 1 bankssts 25 100 40 0 3 | 2 caudalanteriorcingulate 125 100 160 0 4 | 3 caudalmiddlefrontal 100 25 0 0 5 | 4 corpuscallosum 120 70 50 0 6 | 5 cuneus 220 20 100 0 7 | 6 entorhinal 220 20 10 0 8 | 7 fusiform 180 220 140 0 9 | 8 inferiorparietal 220 60 220 0 10 | 9 inferiortemporal 180 40 120 0 11 | 10 isthmuscingulate 140 20 140 0 12 | 11 lateraloccipital 20 30 140 0 13 | 12 lateralorbitofrontal 35 75 50 0 14 | 13 lingual 225 140 140 0 15 | 14 medialorbitofrontal 200 35 75 0 16 | 15 middletemporal 160 100 50 0 17 | 16 parahippocampal 20 220 60 0 18 | 17 paracentral 60 220 60 0 19 | 18 parsopercularis 220 180 140 0 20 | 19 parsorbitalis 20 100 50 0 21 | 20 parstriangularis 220 60 20 0 22 | 21 pericalcarine 120 100 60 0 23 | 22 postcentral 220 20 20 0 24 | 23 posteriorcingulate 220 180 220 0 25 | 24 precentral 60 20 220 0 26 | 25 precuneus 160 140 180 0 27 | 26 rostralanteriorcingulate 80 20 140 0 28 | 27 rostralmiddlefrontal 75 50 125 0 29 | 28 superiorfrontal 20 220 160 0 30 | 29 superiorparietal 20 180 140 0 31 | 30 superiortemporal 140 220 220 0 32 | 31 supramarginal 80 160 20 0 33 | 32 frontalpole 100 0 100 0 34 | 33 temporalpole 70 20 170 0 35 | 34 transversetemporal 150 150 200 0 36 | 35 insula 255 192 32 0 37 | -------------------------------------------------------------------------------- /src/niwidgets/data/example_surfaces/lh.aparc.annot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/example_surfaces/lh.aparc.annot -------------------------------------------------------------------------------- /src/niwidgets/data/example_surfaces/lh.area: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/example_surfaces/lh.area -------------------------------------------------------------------------------- /src/niwidgets/data/example_surfaces/lh.curv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/example_surfaces/lh.curv -------------------------------------------------------------------------------- /src/niwidgets/data/example_surfaces/lh.inflated: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/example_surfaces/lh.inflated -------------------------------------------------------------------------------- /src/niwidgets/data/example_surfaces/lh.thickness: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/example_surfaces/lh.thickness -------------------------------------------------------------------------------- /src/niwidgets/data/streamlines.trk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nipy/niwidgets/421fb1ec57e4670a40b9aae9bc2ca5375043dd39/src/niwidgets/data/streamlines.trk -------------------------------------------------------------------------------- /src/niwidgets/exampledata.py: -------------------------------------------------------------------------------- 1 | """Example files for use with niwidgets.""" 2 | __all__ = [ 3 | "exampleatlas", 4 | "examplezmap", 5 | "examplet1", 6 | "examplesurface", 7 | "exampleoverlays", 8 | "streamlines", 9 | ] 10 | 11 | import os.path 12 | 13 | root_dir = os.path.join(os.path.dirname(__file__), "data") 14 | 15 | exampleatlas = os.path.join(root_dir, "cc400_roi_atlas.nii") 16 | examplezmap = os.path.join(root_dir, "cognitive control_pFgA_z.nii.gz") 17 | examplet1 = os.path.join(root_dir, "T1.nii.gz") 18 | 19 | surface_dir = os.path.join(root_dir, "example_surfaces") 20 | 21 | examplesurface = os.path.join(surface_dir, "lh.inflated") 22 | 23 | exampleoverlays = { 24 | key: os.path.join(surface_dir, f) 25 | for key, f in zip( 26 | ["Area", "Curvature", "Thickness", "Annotation"], 27 | ("lh.area", "lh.curv", "lh.thickness", "lh.aparc.annot"), 28 | ) 29 | } 30 | 31 | streamlines = os.path.join(root_dir, "streamlines.trk") 32 | -------------------------------------------------------------------------------- /src/niwidgets/niwidget_surface.py: -------------------------------------------------------------------------------- 1 | """A widget for surface-warped neuroimaging data.""" 2 | from __future__ import print_function 3 | 4 | import os 5 | from collections import defaultdict 6 | from xml.parsers.expat import ExpatError 7 | 8 | import ipyvolume.pylab as p3 9 | import matplotlib.pyplot as plt 10 | import nibabel as nb 11 | import numpy as np 12 | from IPython.display import display 13 | from ipywidgets import Dropdown, fixed, interact 14 | 15 | from .colormaps import get_cmap_dropdown 16 | 17 | 18 | def _check_file(file): 19 | """Check if file exists and if it's valid""" 20 | if isinstance(file, nb.gifti.gifti.GiftiImage): 21 | return file 22 | elif os.path.isfile(str(file)): 23 | return str(file) 24 | else: 25 | raise ValueError( 26 | "The argument meshfile needs to either be " 27 | "a nibabel Gifti image or an existing file, " 28 | "but " + str(file) + " was provided." 29 | ) 30 | 31 | 32 | def _get_name(file): 33 | """Get a name for the supplied file / giftiimage""" 34 | if isinstance(file, nb.gifti.gifti.GiftiImage): 35 | if file.get_filename(): 36 | return os.path.basename(file.get_filename()) 37 | else: 38 | return str(file) 39 | elif os.path.isfile(str(file)): 40 | return os.path.basename(str(file)) 41 | 42 | 43 | class SurfaceWidget: 44 | """Interact with brain surfaces right in the notebook.""" 45 | 46 | def __init__(self, meshfile, overlayfiles=()): 47 | """Create a surface widget. 48 | 49 | This widget takes in surface data, in the form of gifti files or 50 | freesurfer files, and displays them interactively. 51 | 52 | meshfile : str, Path, nb.gifti.gifti.GiftiImage 53 | A file containing the the surface information. It can either be 54 | a Freesurfer file (i.e. lh.pial) or a .gii mesh file. A loaded 55 | GiftiImage (using nibabel) will also work. 56 | 57 | overlayfiles : tuple, dict, str, nb.gifti.gifti.GiftiImage 58 | Data you'd like to overlay on the 3D mesh. This can be a tuple of 59 | files, a dictionary (in which case the keys of the dict are used 60 | as options in a dropdown menu), or a single file name / loaded 61 | GiftiImage. Possible file formats: .annot, .thickness, .curv, .sulc 62 | or .gii. 63 | 64 | """ 65 | # check meshfile is a valid argument 66 | self.meshfile = _check_file(meshfile) 67 | 68 | # make sure overlayfiles is a dictionary 69 | if isinstance(overlayfiles, dict): 70 | self.overlayfiles = overlayfiles 71 | elif isinstance(overlayfiles, (list, tuple)): 72 | self.overlayfiles = { 73 | _get_name(file): _check_file(file) for file in overlayfiles 74 | } 75 | else: 76 | self.overlayfiles = { 77 | _get_name(overlayfiles): _check_file(overlayfiles) 78 | } 79 | 80 | self.fig = None 81 | 82 | def _init_figure(self, x, y, z, triangles, figsize, figlims): 83 | """ 84 | Initialize the figure by plotting the surface without any overlay. 85 | 86 | x: x coordinates of vertices (V x 1 numpy array) 87 | y: y coordinates of vertices (V x 1 numpy array) 88 | z: z coordinates of vertices (V x 1 numpy array) 89 | triangles: triangle specifications (T x 3 numpy array, where 90 | T=#triangles) 91 | figsize: 2x1 list of integers 92 | figlims: 3x2 list of integers 93 | """ 94 | self.fig = p3.figure(width=figsize[0], height=figsize[1]) 95 | self.fig.camera_fov = 1 96 | self.fig.style = { 97 | "axes": { 98 | "color": "black", 99 | "label": {"color": "black"}, 100 | "ticklabel": {"color": "black"}, 101 | "visible": False, 102 | }, 103 | "background-color": "white", 104 | "box": {"visible": False}, 105 | } 106 | self.fig.xlim = (figlims[0][0], figlims[0][1]) 107 | self.fig.ylim = (figlims[1][0], figlims[1][1]) 108 | self.fig.zlim = (figlims[2][0], figlims[2][1]) 109 | 110 | # draw the tetrahedron 111 | p3.plot_trisurf( 112 | x, y, z, triangles=triangles, color=np.ones((len(x), 3)) 113 | ) 114 | 115 | def _plot_surface( 116 | self, 117 | x, 118 | y, 119 | z, 120 | triangles, 121 | overlays=None, 122 | frame=0, 123 | colormap="summer", 124 | figsize=np.array([600, 600]), 125 | figlims=np.array(3 * [[-100, 100]]), 126 | ): 127 | """ 128 | Visualize/update the overlay. 129 | 130 | This function changes the color associated with mesh vertices. 131 | 132 | overlays: V x F numpy array, where each column corresponds to a 133 | different overlay. F=#frames or #timepoints. 134 | 135 | """ 136 | if self.fig is None: 137 | self._init_figure(x, y, z, triangles, figsize, figlims) 138 | # overlays is a 2D matrix 139 | # with 2nd dimension corresponding to (time) frame 140 | my_color = plt.cm.get_cmap(colormap) 141 | if overlays[frame] is not None: 142 | # activation = overlays[:, frame] 143 | activation = overlays[frame] 144 | if max(activation) - min(activation) > 0: 145 | colors = my_color( 146 | (activation - min(activation)) 147 | / (max(activation) - min(activation)) 148 | ) 149 | else: 150 | colors = my_color((activation - min(activation))) 151 | self.fig.meshes[0].color = colors[:, :3] 152 | 153 | def zmask(surf, mask): 154 | """ 155 | Masks out vertices with intensity=0 from overlay. 156 | 157 | Also returns masked-out vertices. 158 | 159 | Parameters 160 | ---------- 161 | surf: gifti object 162 | already loaded gifti object of target surface 163 | mask: gifti object 164 | already loaded gifti object of overlay with zeroes 165 | at vertices of no interest (e.g. medial wall) 166 | 167 | Returns 168 | ------- 169 | mask_keep : np.ndarray 170 | Boolean array of what to show. 171 | mask_kill : np.ndarray 172 | Boolean array of what to hide. 173 | 174 | """ 175 | ikill = np.flatnonzero(mask.darrays[0].data == 0) 176 | 177 | # create empty arrays matching surface mesh dimentions 178 | mask_kill = np.zeros([surf.darrays[1].data.shape[0]], dtype=bool) 179 | 180 | for ii, row in enumerate(surf.darrays[1].data): 181 | for item in row: 182 | if item in ikill: 183 | mask_kill[ii] = True 184 | 185 | return ~mask_kill, mask_kill 186 | 187 | def surface_plotter( 188 | self, 189 | colormap=None, 190 | figsize=np.array([600, 600]), 191 | figlims=np.array(3 * [[-100, 100]]), 192 | show_zeroes=True, 193 | **kwargs 194 | ): 195 | """Visualise a surface mesh (with overlay) inside notebooks. 196 | 197 | This method displays the surface widget. 198 | 199 | Parameters 200 | ---------- 201 | surface : str, gifti object 202 | Path to surface file in gifti or FS surface format or an 203 | already loaded gifti object of surface 204 | overlay : str, gifti object 205 | Path to overlay file in gifti or FS annot or anatomical 206 | (.curv,.sulc,.thickness) format or an already loaded 207 | gifti object of overlay, default None 208 | colormap : string 209 | A matplotlib colormap, default summer 210 | figsize : ndarray 211 | Size of the figure to display, default [600,600] 212 | figlims : ndarray 213 | x,y and z limits of the axes, default 214 | [[-100,100],[-100,100],[-100,100]] 215 | show_zeroes : bool 216 | Display vertices with intensity = 0, default True 217 | 218 | """ 219 | kwargs["colormap"] = get_cmap_dropdown(colormap) 220 | kwargs["figsize"] = fixed(figsize) 221 | kwargs["figlims"] = fixed(figlims) 222 | 223 | if isinstance(self.meshfile, str): 224 | # if mesh has not been loaded before, load it 225 | if os.path.splitext(self.meshfile)[1].lower() == ".gii": 226 | # load gifti file 227 | try: 228 | self.meshfile = nb.load(self.meshfile) 229 | x, y, z = self.meshfile.darrays[0].data.T 230 | vertex_edges = self.meshfile.darrays[1].data 231 | except ExpatError: 232 | raise ValueError( 233 | "The file {} could not be read. ".format(self.meshfile) 234 | + "Please provide a valid gifti file." 235 | ) 236 | else: 237 | # load freesurfer file 238 | fsgeometry = nb.freesurfer.read_geometry(self.meshfile) 239 | x, y, z = fsgeometry[0].T 240 | vertex_edges = fsgeometry[1] 241 | 242 | elif isinstance(self.meshfile, nb.gifti.gifti.GiftiImage): 243 | # if mesh has been loaded as a GiftiImage, format the data 244 | x, y, z = self.meshfile.darrays[0].data.T 245 | vertex_edges = self.meshfile.darrays[1].data 246 | 247 | overlays = defaultdict(lambda: None) 248 | if len(self.overlayfiles) > 0: 249 | 250 | for key, overlayfile in self.overlayfiles.items(): 251 | 252 | file_ext = os.path.splitext(overlayfile)[1].lower() 253 | 254 | if isinstance(overlayfile, nb.gifti.gifti.GiftiImage): 255 | overlays[key] = overlayfile.darrays[0].data 256 | else: 257 | file_ext = os.path.splitext(overlayfile)[1].lower() 258 | 259 | if file_ext == ".gii": 260 | try: 261 | overlay = nb.load(overlayfile) 262 | overlays[key] = overlay.darrays[0].data 263 | except ExpatError: 264 | raise ValueError( 265 | "The file {} could not be read. ".format( 266 | overlayfile 267 | ) 268 | + "Please provide a valid gifti file." 269 | ) 270 | 271 | elif file_ext in (".annot", ""): 272 | annot = nb.freesurfer.read_annot(overlayfile) 273 | overlays[key] = annot[0] 274 | 275 | elif file_ext in (".curv", ".thickness", ".sulc"): 276 | overlays[key] = nb.freesurfer.read_morph_data( 277 | overlayfile 278 | ) 279 | 280 | if not show_zeroes: 281 | pass 282 | 283 | kwargs["triangles"] = fixed(vertex_edges) 284 | kwargs["x"] = fixed(x) 285 | kwargs["y"] = fixed(y) 286 | kwargs["z"] = fixed(z) 287 | kwargs["overlays"] = fixed(overlays) 288 | 289 | if len(self.overlayfiles) < 2: 290 | frame = fixed(None) 291 | else: 292 | frame = Dropdown( 293 | options=list(self.overlayfiles.keys()), 294 | value=list(self.overlayfiles.keys())[0], 295 | description="Overlay:", 296 | ) 297 | 298 | interact(self._plot_surface, frame=frame, **kwargs) 299 | display(self.fig) 300 | -------------------------------------------------------------------------------- /src/niwidgets/niwidget_volume.py: -------------------------------------------------------------------------------- 1 | """Widgets that visualise volume images in .nii files.""" 2 | import inspect 3 | import os.path 4 | 5 | import ipywidgets as widgets 6 | import matplotlib.pyplot as plt 7 | import nibabel as nib 8 | import numpy as np 9 | import scipy.ndimage 10 | import traitlets 11 | from IPython import display 12 | from ipywidgets import IntSlider, fixed, interact 13 | 14 | from .colormaps import get_cmap_dropdown 15 | from .controls import PlaySlider 16 | 17 | 18 | class NiftiWidget: 19 | """Turn .nii files into interactive plots using ipywidgets. 20 | 21 | Args 22 | ---- 23 | filename : str 24 | The path to your ``.nii`` file. Can be a string, or a 25 | ``PosixPath`` from python3's pathlib. 26 | """ 27 | 28 | def __init__(self, filename): 29 | """ 30 | Turn .nii files into interactive plots using ipywidgets. 31 | 32 | Args 33 | ---- 34 | filename : str 35 | The path to your ``.nii`` file. Can be a string, or a 36 | ``PosixPath`` from python3's pathlib. 37 | """ 38 | if hasattr(filename, "get_data"): 39 | self.data = filename 40 | else: 41 | filename = str(filename) 42 | if not os.path.isfile(filename): 43 | raise OSError("File " + filename + " not found.") 44 | 45 | # load data in advance 46 | # this ensures once the widget is created that the file is of a 47 | # format readable by nibabel 48 | self.data = nib.load(str(filename)) 49 | 50 | # initialise where the image handles will go 51 | self.image_handles = None 52 | 53 | def nifti_plotter( 54 | self, plotting_func=None, colormap=None, figsize=(15, 5), **kwargs 55 | ): 56 | """ 57 | Plot volumetric data. 58 | 59 | Args 60 | ---- 61 | plotting_func : function 62 | A plotting function for .nii files, most likely 63 | mask_background : bool 64 | Whether the background should be masked (set to NA). 65 | This parameter only works in conjunction with the default 66 | plotting function (`plotting_func=None`). It finds clusters 67 | of values that round to zero and somewhere touch the edges 68 | of the image. These are set to NA. If you think you are 69 | missing data in your image, set this False. 70 | colormap : str | list 71 | The matplotlib colormap that should be applied to the data. 72 | By default, the widget will allow you to pick from all that 73 | are available, but you can pass a string to fix the 74 | colormap or a list of strings to offer the user a few 75 | options. 76 | figsize : tup 77 | The figure height and width for matplotlib, in inches. 78 | 79 | If you are providing a custom plot function, any kwargs you provide 80 | to nifti_plotter will be passed to that function. 81 | 82 | """ 83 | kwargs["colormap"] = get_cmap_dropdown(colormap) 84 | kwargs["figsize"] = fixed(figsize) 85 | 86 | if plotting_func is None: 87 | self._default_plotter(**kwargs) 88 | else: 89 | self._custom_plotter(plotting_func, **kwargs) 90 | 91 | def _default_plotter(self, mask_background=False, **kwargs): 92 | """Plot three orthogonal views. 93 | 94 | This is called by nifti_plotter, you shouldn't call it directly. 95 | """ 96 | plt.gcf().clear() 97 | plt.ioff() # disable interactive mode 98 | 99 | data_array = self.data.get_data() 100 | 101 | if not ((data_array.ndim == 3) or (data_array.ndim == 4)): 102 | raise ValueError("Input image should be 3D or 4D") 103 | 104 | # mask the background 105 | if mask_background: 106 | # TODO: add the ability to pass 'mne' to use a default brain mask 107 | # TODO: split this out into a different function 108 | if data_array.ndim == 3: 109 | labels, n_labels = scipy.ndimage.measurements.label( 110 | (np.round(data_array) == 0) 111 | ) 112 | else: # 4D 113 | labels, n_labels = scipy.ndimage.measurements.label( 114 | (np.round(data_array).max(axis=3) == 0) 115 | ) 116 | 117 | mask_labels = [ 118 | lab 119 | for lab in range(1, n_labels + 1) 120 | if ( 121 | np.any(labels[[0, -1], :, :] == lab) 122 | | np.any(labels[:, [0, -1], :] == lab) 123 | | np.any(labels[:, :, [0, -1]] == lab) 124 | ) 125 | ] 126 | 127 | if data_array.ndim == 3: 128 | data_array = np.ma.masked_where( 129 | np.isin(labels, mask_labels), data_array 130 | ) 131 | else: 132 | data_array = np.ma.masked_where( 133 | np.broadcast_to( 134 | np.isin(labels, mask_labels)[:, :, :, np.newaxis], 135 | data_array.shape, 136 | ), 137 | data_array, 138 | ) 139 | 140 | # init sliders for the various dimensions 141 | for dim, label in enumerate(["x", "y", "z"]): 142 | if label not in kwargs.keys(): 143 | kwargs[label] = IntSlider( 144 | value=(data_array.shape[dim] - 1) / 2, 145 | min=0, 146 | max=data_array.shape[dim] - 1, 147 | continuous_update=False, 148 | ) 149 | 150 | if (data_array.ndim == 3) or (data_array.shape[3] == 1): 151 | kwargs["t"] = fixed(None) # time is fixed 152 | else: 153 | kwargs["t"] = IntSlider( 154 | value=0, 155 | min=0, 156 | max=data_array.shape[3] - 1, 157 | continuous_update=False, 158 | ) 159 | 160 | widgets.interact(self._plot_slices, data=fixed(data_array), **kwargs) 161 | 162 | plt.close() # clear plot 163 | plt.ion() # return to interactive state 164 | 165 | def _plot_slices( 166 | self, data, x, y, z, t, colormap="viridis", figsize=(15, 5) 167 | ): 168 | """ 169 | Plot x,y,z slices. 170 | 171 | This function is called by _default_plotter 172 | """ 173 | fresh = self.image_handles is None 174 | if fresh: 175 | self._init_figure(data, colormap, figsize) 176 | 177 | coords = [x, y, z] 178 | 179 | # add plot titles to the subplots 180 | views = ["Sagittal", "Coronal", "Axial"] 181 | for i, ax in enumerate(self.fig.axes): 182 | ax.set_title(views[i]) 183 | 184 | for ii, imh in enumerate(self.image_handles): 185 | 186 | slice_obj = 3 * [slice(None)] 187 | 188 | if data.ndim == 4: 189 | slice_obj.append(t) 190 | 191 | slice_obj[ii] = coords[ii] 192 | 193 | # update the image 194 | imh.set_data( 195 | np.flipud(np.rot90(data[tuple(slice_obj)], k=1)) 196 | if views[ii] != "Sagittal" 197 | else np.fliplr( 198 | np.flipud(np.rot90(data[tuple(slice_obj)], k=1)) 199 | ) 200 | ) 201 | 202 | # draw guides to show selected coordinates 203 | guide_positions = [ 204 | val for jj, val in enumerate(coords) if jj != ii 205 | ] 206 | imh.axes.lines[0].set_xdata(2 * [guide_positions[0]]) 207 | imh.axes.lines[1].set_ydata(2 * [guide_positions[1]]) 208 | 209 | imh.set_cmap(colormap) 210 | 211 | if not fresh: 212 | return self.fig 213 | 214 | def _init_figure(self, data, colormap, figsize): 215 | # init an empty list 216 | self.image_handles = [] 217 | # open the figure 218 | self.fig, axes = plt.subplots(1, 3, figsize=figsize) 219 | 220 | for ii, ax in enumerate(axes): 221 | 222 | ax.set_facecolor("black") 223 | 224 | ax.tick_params( 225 | axis="both", 226 | which="both", 227 | bottom="off", 228 | top="off", 229 | labelbottom="off", 230 | right="off", 231 | left="off", 232 | labelleft="off", 233 | ) 234 | # fix the axis limits 235 | axis_limits = [ 236 | limit for jj, limit in enumerate(data.shape[:3]) if jj != ii 237 | ] 238 | ax.set_xlim(0, axis_limits[0]) 239 | ax.set_ylim(0, axis_limits[1]) 240 | 241 | img = np.zeros(axis_limits[::-1]) 242 | # img[1] = data_max 243 | im = ax.imshow( 244 | img, cmap=colormap, vmin=data.min(), vmax=data.max() 245 | ) 246 | # add "cross hair" 247 | ax.axvline(x=0, color="gray", alpha=0.8) 248 | ax.axhline(y=0, color="gray", alpha=0.8) 249 | # append to image handles 250 | self.image_handles.append(im) 251 | # plt.show() 252 | 253 | def _custom_plotter(self, plotting_func, **kwargs): 254 | """Collect data and start interactive widget for custom plot.""" 255 | self.plotting_func = plotting_func 256 | plt.gcf().clear() 257 | plt.ioff() 258 | 259 | # XYZ Sliders if plot supports it and user didn't provide any: 260 | if ( 261 | "cut_coords" in inspect.getargspec(self.plotting_func)[0] 262 | and "cut_coords" not in kwargs.keys() 263 | ): 264 | for label in ["x", "y", "z"]: 265 | if label not in kwargs.keys(): 266 | # cut_coords should be given in MNI coordinates 267 | kwargs[label] = IntSlider( 268 | value=0, min=-90, max=90, continuous_update=False 269 | ) 270 | 271 | # Create the widget: 272 | interact(self._custom_plot_wrapper, data=fixed(self.data), **kwargs) 273 | plt.ion() 274 | 275 | def _custom_plot_wrapper(self, data, **kwargs): 276 | """Wrap a custom function.""" 277 | # start the figure 278 | fig = plt.figure(figsize=kwargs.pop("figsize", None)) 279 | 280 | # The following should provide a colormap option to most plots: 281 | if "colormap" in kwargs.keys(): 282 | if "cmap" in inspect.getargspec(self.plotting_func)[0]: 283 | # if cmap is valid argument to plot func, rename colormap 284 | kwargs["cmap"] = kwargs.pop("colormap") 285 | else: 286 | # if cmap is not valid for plot func, try and coerce it 287 | plt.set_cmap(kwargs.pop("colormap")) 288 | 289 | # reconstruct manually added x-y-z-sliders: 290 | if ( 291 | "cut_coords" in inspect.getargspec(self.plotting_func)[0] 292 | and "x" in kwargs.keys() 293 | ): 294 | 295 | # add the x-y-z as cut_coords 296 | if "display_mode" not in kwargs.keys() or not any( 297 | [label in kwargs["display_mode"] for label in ["x", "y", "z"]] 298 | ): 299 | # If no xyz combination of display modes was requested: 300 | kwargs["cut_coords"] = [ 301 | kwargs[label] for label in ["x", "y", "z"] 302 | ] 303 | else: 304 | kwargs["cut_coords"] = [ 305 | kwargs[label] 306 | for label in ["x", "y", "z"] 307 | if label in kwargs["display_mode"] 308 | ] 309 | # remove x-y-z from kwargs 310 | [kwargs.pop(label, None) for label in ["x", "y", "z"]] 311 | 312 | # Actually plot the image 313 | self.plotting_func(data, figure=fig, **kwargs) 314 | plt.show() 315 | 316 | 317 | class VolumeWidget(traitlets.HasTraits): 318 | """Turn .nii files into interactive plots using ipywidgets. 319 | 320 | Args 321 | ---- 322 | filename : str 323 | The path to your ``.nii`` file. Can be a string, or a 324 | ``PosixPath`` from python3's pathlib. 325 | """ 326 | 327 | # the indices are traitlets that can be linked: 328 | x = traitlets.Integer(0) 329 | y = traitlets.Integer(0) 330 | z = traitlets.Integer(0) 331 | t = traitlets.Integer(0) 332 | colormap = traitlets.Unicode("summer") 333 | reverse_colors = traitlets.Bool(False) 334 | 335 | guidelines = traitlets.Bool(True) 336 | orient_radiology = traitlets.Bool(True) 337 | 338 | def __init__( 339 | self, 340 | filename, 341 | figsize=(5, 5), 342 | colormap=None, 343 | orient_radiology=None, 344 | guidelines=None, 345 | animation_speed=300, 346 | ): 347 | """ 348 | Turn .nii files into interactive plots using ipywidgets. 349 | 350 | Args 351 | ---- 352 | filename : str 353 | The path to your ``.nii`` file. Can be a string, or a 354 | ``PosixPath`` from python3's pathlib. 355 | figsize : tuple 356 | The figure size for each individual view. 357 | colormaps : tuple, list 358 | A list of colormaps, or single colormap. 359 | If None, a selection of maps is used to populate the 360 | dropdown widget. 361 | orient_radiology : bool 362 | Whether to display the images in the "radiological" style, 363 | i.e. with lef and right reversed. If None, a checkbox is 364 | offered. 365 | guidelines : bool 366 | Whether to display guide lines. If None, a checkbox is 367 | offered. 368 | animation_speed : int 369 | The speed used for the animation, in milliseconds between 370 | frames (the lower, the faster, up to a limit). 371 | """ 372 | 373 | if hasattr(filename, "get_data"): 374 | self.data = filename 375 | else: 376 | filename = str(filename) 377 | if not os.path.isfile(filename): 378 | raise OSError("File " + filename + " not found.") 379 | # load data to ensures that the file readable by nibabel 380 | self.data = nib.load(str(filename)) 381 | 382 | self.displays = [widgets.Output() for _ in range(3)] 383 | self._generate_axes(figsize=figsize) 384 | 385 | # set how many dimensions this file has 386 | self.ndim = len(self.data.shape) 387 | 388 | # initialise the control components of this widget 389 | self.dims = ["x", "y", "z", "t"][: self.ndim] 390 | self.controls = {} 391 | for i, dim in enumerate(self.dims): 392 | maxval = self.data.shape[i] - 1 393 | self.controls[dim] = PlaySlider( 394 | min=0, 395 | max=maxval, 396 | value=maxval // 2, 397 | interval=animation_speed, 398 | label=dim.upper(), 399 | continuous_update=False, 400 | ) 401 | widgets.link((self.controls[dim], "value"), (self, dim)) 402 | 403 | if not isinstance(colormap, str): 404 | self.color_picker = get_cmap_dropdown(colormap) 405 | widgets.link((self.color_picker, "value"), (self, "colormap")) 406 | self.color_reverser = widgets.Checkbox( 407 | description="Reverse colormap", indent=True 408 | ) 409 | widgets.link( 410 | (self.color_reverser, "value"), (self, "reverse_colors") 411 | ) 412 | else: 413 | self.color_picker = widgets.HBox([]) 414 | self.color_reverser = widgets.HBox([]) 415 | self.colormap = colormap 416 | 417 | if guidelines is None: 418 | self.guideline_picker = widgets.Checkbox( 419 | value=True, description="Show guides", indent=False 420 | ) 421 | widgets.link( 422 | (self.guideline_picker, "value"), (self, "guidelines") 423 | ) 424 | else: 425 | self.guideline_picker = widgets.Box([]) 426 | self.guidelines = guidelines 427 | 428 | if orient_radiology is None: 429 | self.orientation_switcher = widgets.Checkbox( 430 | value=self.orient_radiology, 431 | indent=False, 432 | description="Radiological Orientation", 433 | ) 434 | widgets.link( 435 | (self.orientation_switcher, "value"), 436 | (self, "orient_radiology"), 437 | ) 438 | else: 439 | self.orientation_switcher = widgets.Box([]) 440 | self.orient_radiology = orient_radiology 441 | 442 | self._update_orientation(True) 443 | 444 | @property 445 | def indices(self): 446 | return [self.x, self.y, self.z, self.t][: self.ndim] 447 | 448 | @traitlets.observe("x", "y", "z", "t", "guidelines", "orient_radiology") 449 | def _update_slices(self, change): 450 | array = self.data.get_data() 451 | 452 | if self.ndim == 3: 453 | array = array[:, :, :, np.newaxis] 454 | 455 | for iimage, (disp, fig, ax, image) in enumerate( 456 | zip(self.displays, self.figures, self.axes, self.images) 457 | ): 458 | image_data = np.rot90( 459 | array[ 460 | tuple( 461 | slice(None) if iimage != idim else self.indices[idim] 462 | for idim in range(3) 463 | ) 464 | + (self.t,) 465 | ] 466 | ) 467 | if image is not None: 468 | image.set_data(image_data) 469 | else: 470 | self.images[iimage] = ax.imshow( 471 | image_data, 472 | cmap=self.colormap, 473 | vmin=self.data.get_data().min(), 474 | vmax=self.data.get_data().max(), 475 | ) 476 | 477 | if self.guidelines: 478 | # add "cross hair" 479 | x_idx, y_idx = (i for i in range(3) if i != iimage) 480 | x = self.indices[x_idx] 481 | y = self.data.shape[y_idx] - self.indices[y_idx] 482 | ax.lines = [ 483 | ax.axvline(x=x, color="gray"), 484 | ax.axhline(y=y, color="gray"), 485 | ] 486 | else: 487 | ax.lines = [] 488 | 489 | self._redraw() 490 | 491 | @traitlets.observe("orient_radiology") 492 | def _update_orientation(self, change): 493 | for i, ax in enumerate(self.axes): 494 | if change is not None: 495 | ax.invert_xaxis() 496 | ax.texts = [] 497 | if self.orient_radiology: 498 | if i == 0: 499 | ax.text( 500 | -0.01, 501 | 0.5, 502 | "F", 503 | transform=ax.transAxes, 504 | horizontalalignment="right", 505 | verticalalignment="center", 506 | fontsize=16, 507 | ) 508 | ax.text( 509 | 1.01, 510 | 0.5, 511 | "P", 512 | transform=ax.transAxes, 513 | horizontalalignment="left", 514 | verticalalignment="center", 515 | fontsize=16, 516 | ) 517 | elif i == 1 or i == 2: 518 | ax.text( 519 | -0.01, 520 | 0.5, 521 | "R", 522 | transform=ax.transAxes, 523 | horizontalalignment="right", 524 | verticalalignment="center", 525 | fontsize=16, 526 | ) 527 | ax.text( 528 | 1.01, 529 | 0.5, 530 | "L", 531 | transform=ax.transAxes, 532 | horizontalalignment="left", 533 | verticalalignment="center", 534 | fontsize=16, 535 | ) 536 | else: 537 | if i == 0: 538 | ax.text( 539 | -0.01, 540 | 0.5, 541 | "P", 542 | transform=ax.transAxes, 543 | horizontalalignment="right", 544 | verticalalignment="center", 545 | fontsize=16, 546 | ) 547 | ax.text( 548 | 1.01, 549 | 0.5, 550 | "F", 551 | transform=ax.transAxes, 552 | horizontalalignment="left", 553 | verticalalignment="center", 554 | fontsize=16, 555 | ) 556 | elif i == 1 or i == 2: 557 | ax.text( 558 | -0.01, 559 | 0.5, 560 | "L", 561 | transform=ax.transAxes, 562 | horizontalalignment="right", 563 | verticalalignment="center", 564 | fontsize=16, 565 | ) 566 | ax.text( 567 | 1.01, 568 | 0.5, 569 | "R", 570 | transform=ax.transAxes, 571 | horizontalalignment="left", 572 | verticalalignment="center", 573 | fontsize=16, 574 | ) 575 | 576 | self._redraw() 577 | 578 | @traitlets.observe("colormap", "reverse_colors") 579 | def _update_colormap(self, change): 580 | for image in self.images: 581 | if self.reverse_colors: 582 | image.set_cmap(self.colormap + "_r") 583 | else: 584 | image.set_cmap(self.colormap) 585 | self._redraw() 586 | 587 | def _generate_axes(self, figsize=(5, 5)): 588 | plt.ioff() # to avoid figure duplication 589 | self.figures, self.axes = zip( 590 | *[plt.subplots(1, 1, figsize=figsize) for _ in range(3)] 591 | ) 592 | 593 | self.images = [None] * 3 594 | for ax, title in zip(self.axes, ["Sagittal", "Coronal", "Axial"]): 595 | ax.set_title(title) 596 | ax.set_axis_off() 597 | 598 | def _redraw(self): 599 | for fig, disp in zip(self.figures, self.displays): 600 | with disp: 601 | display.clear_output(wait=True) 602 | display.display(fig) 603 | 604 | def render(self): 605 | """Build the widget view and return it.""" 606 | self.layout = widgets.VBox( 607 | [ 608 | widgets.Box( 609 | [self.controls[dim] for dim in self.dims], 610 | layout={"flex_flow": "row wrap"}, 611 | ), 612 | widgets.HBox([self.color_picker]), 613 | widgets.HBox( 614 | [ 615 | self.color_reverser, 616 | self.guideline_picker, 617 | self.orientation_switcher, 618 | ], 619 | layout={"flex_flow": "row wrap"}, 620 | ), 621 | widgets.Box(self.displays, layout={"flex_flow": "row wrap"}), 622 | ] 623 | ) 624 | return self.layout 625 | 626 | def _ipython_display_(self): 627 | """This gets called instead of __repr__ in ipython.""" 628 | display.display(self.render()) 629 | 630 | def close(self): 631 | """Close all figures created for this widget.""" 632 | for fig in self.figures: 633 | plt.close(fig) # to avoid matplotlib warnings 634 | 635 | def __del__(self): 636 | """Method to tidy up afterwards.""" 637 | self.close() 638 | -------------------------------------------------------------------------------- /src/niwidgets/streamlines.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | 5 | import ipyvolume as ipv 6 | import nibabel as nib 7 | import numpy as np 8 | from ipywidgets import fixed, interact, widgets 9 | 10 | 11 | def length(x): 12 | """Returns the sum of euclidean distances between neighboring points""" 13 | return np.sum( 14 | np.sqrt( 15 | np.sum((x[:-1, :] - x[1:, :]) * (x[:-1, :] - x[1:, :]), axis=1) 16 | ) 17 | ) 18 | 19 | 20 | def color(x): 21 | """Returns an approximation for color of line based on its endpoints""" 22 | dirvec = x[0, :] - x[-1, :] 23 | return (dirvec / (np.sqrt(np.sum(dirvec * dirvec, axis=-1)))).dot( 24 | np.eye(3) 25 | ) 26 | 27 | 28 | class StreamlineWidget: 29 | """ 30 | Turns nibabel track files into interactive plots using ipyvolume. 31 | 32 | Color for each line is rendered as a function of the endpoints of the 33 | streamline. Currently, this resolves to red for left-right, green for 34 | anterior-posterior, and blue for inferior-superior. 35 | 36 | Args 37 | ---- 38 | filename : str, pathlib.Path 39 | The path to your ``.trk`` file. Can be a string, or a 40 | ``PosixPath`` from python3's pathlib. 41 | streamlines : a nibabel streamline object 42 | An streamlines attribute of an object loaded by 43 | nibabel.streamlines.load 44 | """ 45 | 46 | def __init__(self, filename=None, streamlines=None): 47 | 48 | if filename: 49 | filename = str(filename) 50 | if not os.path.isfile(filename): 51 | # for Python3 should have FileNotFoundError here 52 | raise IOError("file {} not found".format(filename)) 53 | 54 | if not nib.streamlines.is_supported(filename): 55 | raise ValueError( 56 | ( 57 | "File {0} is not a streamline file supported" 58 | " by nibabel" 59 | ).format(filename) 60 | ) 61 | 62 | # load data in advance 63 | self.streamlines = nib.streamlines.load(filename).streamlines 64 | elif streamlines: 65 | self.streamlines = streamlines 66 | else: 67 | raise ValueError( 68 | "One of filename or streamlines must be specified" 69 | ) 70 | self.lines2use_ = None 71 | 72 | def plot(self, display_fraction=0.1, **kwargs): 73 | """ 74 | This is the main method for this widget. 75 | 76 | Args 77 | ---- 78 | display_fraction : float 79 | The fraction of streamlines to show 80 | percentile : int 81 | The initial number of streamlines to show using a 82 | percentile of length distribution 83 | width : int 84 | The width of the figure 85 | height : int 86 | The height of the figure 87 | """ 88 | 89 | if display_fraction is not None and ( 90 | display_fraction > 1 or display_fraction <= 0 91 | ): 92 | raise ValueError( 93 | "proportion_to_display is a float between 0 and 1" 94 | " (0 excluded) or None" 95 | ) 96 | 97 | N = len(self.streamlines) 98 | num_streamlines = int(display_fraction * N) 99 | indices = np.random.permutation(N)[:num_streamlines] 100 | self.lines2use_ = self.streamlines[indices] 101 | self._default_plotter(**kwargs) 102 | 103 | def _create_mesh(self, indices2use=None): 104 | if indices2use is None: 105 | lines2use = self.lines2use_ 106 | local_colors = self.colors 107 | else: 108 | lines2use = self.lines2use_[indices2use] 109 | local_colors = self.colors[indices2use] 110 | x, y, z = np.concatenate(lines2use).T 111 | 112 | # will contain indices to the verties, [0, 1, 1, 2, 2, 3, 3, 4, 4, 5..] 113 | indices = np.zeros( 114 | np.sum((len(line) - 1) * 2 for line in lines2use), dtype=np.uint32 115 | ) 116 | colors = np.zeros((len(x), 3), dtype=np.float32) 117 | 118 | vertex_offset = 0 119 | line_offset = 0 120 | line_pointers = [] 121 | # if we have a line of 4 vertices, we need to add the indices: 122 | # offset + [0, 1, 1, 2, 2, 3] 123 | # so we have approx 2x the number of indices compared to vertices 124 | for idx, line in enumerate(lines2use): 125 | line_length = len(line) 126 | # repeat all but the start and end vertex 127 | line_indices = np.repeat( 128 | np.arange( 129 | vertex_offset, 130 | vertex_offset + line_length, 131 | dtype=indices.dtype, 132 | ), 133 | 2, 134 | )[1:-1] 135 | indices[ 136 | line_offset : line_offset + line_length * 2 - 2 137 | ] = line_indices 138 | line_pointers.append([line_offset, line_length, line_indices]) 139 | colors[vertex_offset : vertex_offset + line_length] = local_colors[ 140 | idx 141 | ] 142 | line_offset += line_length * 2 - 2 143 | vertex_offset += line_length 144 | return x, y, z, indices, colors, line_pointers 145 | 146 | def _default_plotter(self, **kwargs): 147 | """ 148 | Basic plot function to be used if no custom function is specified. 149 | 150 | This is called by plot, you shouldn't call it directly. 151 | """ 152 | 153 | self.lengths = np.array([length(x) for x in self.lines2use_]) 154 | if not ("grayscale" in kwargs and kwargs["grayscale"]): 155 | self.colors = np.array([color(x) for x in self.lines2use_]) 156 | else: 157 | self.colors = np.zeros((len(self.lines2use_), 3), dtype=np.float16) 158 | self.colors[:] = [0.5, 0.5, 0.5] 159 | self.state = {"threshold": 0, "indices": []} 160 | 161 | width = 600 162 | height = 600 163 | perc = 80 164 | if "width" in kwargs: 165 | width = kwargs["width"] 166 | if "height" in kwargs: 167 | height = kwargs["height"] 168 | if "percentile" in kwargs: 169 | perc = kwargs["percentile"] 170 | 171 | ipv.clear() 172 | fig = ipv.figure(width=width, height=height) 173 | self.state["fig"] = fig 174 | 175 | with fig.hold_sync(): 176 | x, y, z, indices, colors, self.line_pointers = self._create_mesh() 177 | limits = np.array( 178 | [ 179 | min([x.min(), y.min(), z.min()]), 180 | max([x.max(), y.max(), z.max()]), 181 | ] 182 | ) 183 | mesh = ipv.Mesh(x=x, y=y, z=z, lines=indices, color=colors) 184 | fig.meshes = [mesh] 185 | if "style" not in kwargs: 186 | fig.style = { 187 | "axes": { 188 | "color": "black", 189 | "label": {"color": "black"}, 190 | "ticklabel": {"color": "black"}, 191 | "visible": False, 192 | }, 193 | "background-color": "white", 194 | "box": {"visible": False}, 195 | } 196 | else: 197 | fig.style = kwargs["style"] 198 | ipv.pylab._grow_limits(limits, limits, limits) 199 | fig.camera_fov = 1 200 | ipv.show() 201 | 202 | interact( 203 | self._plot_lines, 204 | state=fixed(self.state), 205 | threshold=widgets.FloatSlider( 206 | value=np.percentile(self.lengths, perc), 207 | min=self.lengths.min() - 1, 208 | max=self.lengths.max() - 1, 209 | continuous_update=False, 210 | ), 211 | ) 212 | 213 | def _plot_lines(self, state, threshold): 214 | """ 215 | Plots streamlines 216 | 217 | This function is called by _default_plotter 218 | """ 219 | if threshold < state["threshold"]: 220 | # when threshold is reduced, increase the number of lines 221 | state["indices"] = np.where(self.lengths > threshold)[0] 222 | with state["fig"].hold_sync(): 223 | mesh = state["fig"].meshes[0] 224 | copy = mesh.lines.copy() 225 | for idx in state["indices"]: 226 | ( 227 | line_offset, 228 | line_length, 229 | line_indices, 230 | ) = self.line_pointers[idx] 231 | copy[ 232 | line_offset : line_offset + line_length * 2 - 2 233 | ] = line_indices 234 | mesh.lines = copy 235 | mesh.send_state("lines") 236 | else: 237 | # when threshold is increased, decrease the number of lines 238 | indices = np.where(self.lengths <= threshold)[0] 239 | with state["fig"].hold_sync(): 240 | mesh = state["fig"].meshes[0] 241 | copy = mesh.lines.copy() 242 | for idx in indices: 243 | ( 244 | line_offset, 245 | line_length, 246 | line_indices, 247 | ) = self.line_pointers[idx] 248 | copy[line_offset : line_offset + line_length * 2 - 2] = 0 249 | mesh.lines = copy 250 | mesh.send_state("lines") 251 | state["indices"] = np.where(self.lengths > threshold)[0] 252 | state["threshold"] = threshold 253 | -------------------------------------------------------------------------------- /src/niwidgets/version.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | __version__ = pkg_resources.get_distribution("niwidgets").version 4 | version = __version__ 5 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # pytest_plugins = ["helpers_namespace"] # noqa 2 | 3 | # fix matplotlib import errors on Mac OS: 4 | import sys 5 | 6 | if sys.platform == "darwin": # noqa 7 | import matplotlib # noqa 8 | 9 | matplotlib.use("PS") # noqa 10 | 11 | import warnings 12 | import collections 13 | import pytest 14 | 15 | import numpy as np 16 | 17 | # import pandas as pd 18 | 19 | # from hypothesis.errors import HypothesisDeprecationWarning 20 | # from hypothesis import settings, HealthCheck 21 | 22 | # warnings.simplefilter("ignore", HypothesisDeprecationWarning) 23 | 24 | # settings.register_profile( 25 | # "travis-ci", suppress_health_check=(HealthCheck.too_slow,), deadline=1500 26 | # ) 27 | 28 | # settings.load_profile("travis-ci") 29 | 30 | 31 | # @pytest.helpers.register 32 | # def same_elements(a, b): 33 | # """Test if two things have the same elements, in different orders.""" 34 | # return collections.Counter(a) == collections.Counter(b) 35 | 36 | 37 | # @pytest.helpers.register 38 | # def no_shared_members(a, b): 39 | # return (set(a) & set(b)) == set() 40 | 41 | 42 | # @pytest.helpers.register 43 | # def exact_element_match(a, b): 44 | # if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): 45 | # try: 46 | # return ((a == b) | (np.isnan(a) & np.isnan(b))).all() 47 | # except TypeError: 48 | # return (a == b).all() 49 | # elif isinstance(a, pd.DataFrame) and isinstance(b, pd.DataFrame): 50 | # a = a.reset_index(drop=True) 51 | # b = b.reset_index(drop=True) 52 | # return ((a == b) | (a.isnull() & b.isnull())).all().all() or a.empty or b.empty 53 | # else: 54 | # return all( 55 | # [a_ == b_ or (np.isnan(a_) and np.isnan(b_)) for a_, b_ in zip(a, b)] 56 | # ) 57 | 58 | 59 | # @pytest.helpers.register 60 | # def recursively_list_widget_children(parent): 61 | # childless = [ 62 | # widget for widget in parent.children if not hasattr(widget, "children") 63 | # ] 64 | # parents = [widget for widget in parent.children if hasattr(widget, "children")] 65 | # for widget in parents: 66 | # childless += recursively_list_widget_children(widget) 67 | 68 | # return childless 69 | -------------------------------------------------------------------------------- /tests/test_surface.py: -------------------------------------------------------------------------------- 1 | from niwidgets import SurfaceWidget 2 | from niwidgets.exampledata import examplesurface 3 | 4 | 5 | def test_creation(): 6 | SurfaceWidget(examplesurface) 7 | -------------------------------------------------------------------------------- /tests/test_volume.py: -------------------------------------------------------------------------------- 1 | from niwidgets import NiftiWidget, examplet1 2 | 3 | 4 | def test_creation(): 5 | test_widget = NiftiWidget(examplet1) 6 | --------------------------------------------------------------------------------