Skip to content

Commit

Permalink
Merge branch 'master' into add_time_grid_widget
Browse files Browse the repository at this point in the history
  • Loading branch information
bendichter committed Jul 18, 2023
2 parents f7a4add + 6fd38ad commit 8b8d6b4
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 69 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Upcoming

### New features
* Improved browsing and reading local NWB files with Panel, using ipyfilechooser [PR #300](https://github.com/NeurodataWithoutBorders/nwbwidgets/pull/300)
* Improve readibility of dandisets and files dropdown lists [PR #301](https://github.com/NeurodataWithoutBorders/nwbwidgets/pull/301)

### Fixes
* Fix I/O issues when streaming data on Panel [PR #295](https://github.com/NeurodataWithoutBorders/nwbwidgets/pull/295)
* Fix plotly Figure not showing up, pinned Plotly version [PR #297](https://github.com/NeurodataWithoutBorders/nwbwidgets/pull/297)
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
# NWB Widgets
A library of widgets for visualization NWB data in a Jupyter notebook (or lab). The widgets allow you to navigate through the hierarchical structure of the NWB file and visualize specific data elements. It is designed to work out-of-the-box with NWB 2.0 files and to be easy to extend.

[![PyPI version](https://badge.fury.io/py/nwbwidgets.svg)](https://badge.fury.io/py/nwbwidgets)
[![codecov](https://codecov.io/gh/NeurodataWithoutBorders/nwbwidgets/branch/master/graph/badge.svg)](https://codecov.io/gh/NeurodataWithoutBorders/nwbwidgets)
[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/NeurodataWithoutBorders/nwb-jupyter-widgets/master?filepath=examples%2FNWBWidgets-modality-demos.ipynb)

<p align="center">
<img src="https://github.com/NeurodataWithoutBorders/nwbwidgets/assets/844306/f20b8c26-79c7-4c1c-a3b5-b49ecf8cce5d" width="350" alt="NWB Widgets"/>
<h3 align="center">Explore NWB data in Jupyter</h3>
</p>
<p align="center">
<a href="https://nwbwidgets.readthedocs.io/"><strong>Explore our documentation »</strong></a>
</p>

<!-- TABLE OF CONTENTS -->

## Table of Contents

- [About](#about)
- [Installation](#installation)
- [Usage](#usage)
- [Demo](#demo)
- [Documentation](#documentation)

## About
A library of widgets for visualization NWB data in a Jupyter notebook (or lab). The widgets allow you to navigate through the hierarchical structure of the NWB file and visualize specific data elements. It is designed to work out-of-the-box with NWB 2.0 files and to be easy to extend.


## Installation
Expand Down Expand Up @@ -37,6 +54,9 @@ nwb2widget(nwb)
## Demo
![](https://drive.google.com/uc?export=download&id=1JtI2KtT8MielIMvvtgxRzFfBTdc41LiE)

## Documentation
See our [ReadTheDocs page](https://nwbwidgets.readthedocs.io/en/main/) for full documentation, including a gallery of all supported formats.

## How it works
All visualizations are controlled by the dictionary `neurodata_vis_spec`. The keys of this dictionary are pynwb neurodata types, and the values are functions that take as input that neurodata_type and output a visualization. The visualizations may be of type `Widget` or `matplotlib.Figure`. When you enter a neurodata_type instance into `nwb2widget`, it searches the `neurodata_vis_spec` for that instance's neurodata_type, progressing backwards through the parent classes of the neurodata_type to find the most specific neurodata_type in `neurodata_vis_spec`. Some of these types are containers for other types, and create accordian UI elements for its contents, which are then passed into the `neurodata_vis_spec` and rendered accordingly.

Expand Down
80 changes: 16 additions & 64 deletions nwbwidgets/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ipywidgets as widgets
from dandi.dandiapi import DandiAPIClient
from fsspec.implementations.cached import CachingFileSystem
from ipyfilechooser import FileChooser
from pynwb import NWBHDF5IO
from tqdm.notebook import tqdm

Expand Down Expand Up @@ -48,7 +49,6 @@ def __init__(

self.source_options_names = list()
if enable_local_source:
self.source_options_names.append("Local dir")
self.source_options_names.append("Local file")
if enable_dandi_source:
self.source_options_names.append("DANDI")
Expand Down Expand Up @@ -83,8 +83,8 @@ def __init__(
self.source_options_radio.observe(self.updated_source, "value")

if enable_local_source:
self.source_options_radio.value = "Local dir"
self.create_components_local_dir_source()
self.source_options_radio.value = "Local file"
self.create_components_local_file_source()
elif enable_dandi_source:
self.source_options_radio.value = "DANDI"
self.create_components_dandi_source()
Expand All @@ -95,8 +95,6 @@ def updated_source(self, args=None):
self.create_components_dandi_source()
elif args["new"] == "S3":
self.create_components_s3_source()
elif args["new"] == "Local dir":
self.create_components_local_dir_source()
elif args["new"] == "Local file":
self.create_components_local_file_source()

Expand All @@ -111,7 +109,7 @@ def create_components_dandi_source(self, args=None):
dandiset_options.append(item_name)

self.source_dandi_id = widgets.Dropdown(
options=dandiset_options,
options=sorted(dandiset_options),
description="Dandiset:",
layout=widgets.Layout(width="400px", overflow=None),
)
Expand Down Expand Up @@ -182,69 +180,28 @@ def create_components_s3_source(self):
self.source_s3_button.on_click(self.stream_s3_file)
self.cache_checkbox.observe(self.toggle_cache)

def create_components_local_dir_source(self):
"""Create widgets components for Loca dir option"""
self.local_dir_path = widgets.Text(
value="",
description="Dir path:",
layout=widgets.Layout(width="400px", overflow=None),
)
self.local_dir_button = widgets.Button(description="Search")
self.local_dir_top = widgets.HBox(
children=[self.local_dir_path, self.local_dir_button],
layout=widgets.Layout(padding="5px 0px 5px 0px"),
)
self.local_dir_files = widgets.Dropdown(
options=[],
description="Files:",
layout=widgets.Layout(width="400px", overflow=None),
)
self.local_dir_file_button = widgets.Button(icon="check", description="Load file")
self.local_dir_panel = widgets.VBox(
children=[
self.local_dir_top,
self.local_dir_files,
self.local_dir_file_button,
],
layout=widgets.Layout(padding="5px 0px 5px 0px"),
)
self.source_changing_panel.children = [self.local_dir_panel]
self.local_dir_button.on_click(self.list_local_dir_files)
self.local_dir_file_button.on_click(self.load_local_dir_file)

def create_components_local_file_source(self):
"""Create widgets components for Local file option"""
self.local_file_path = widgets.Text(
value="",
description="File path:",
layout=widgets.Layout(width="400px", overflow=None),
)
self.local_file_button = widgets.Button(icon="check", description="Load file")
self.local_file_chooser = FileChooser()
self.local_file_chooser.sandbox_path = str(Path.cwd())
self.local_file_chooser.filter_pattern = ["*.nwb"]
self.local_file_chooser.title = "<b>Select local NWB file</b>"
self.local_file_chooser.register_callback(self.load_local_file)
self.local_file_panel = widgets.VBox(
children=[self.local_file_path, self.local_file_button],
children=[self.local_file_chooser],
layout=widgets.Layout(padding="5px 0px 5px 0px"),
)
self.source_changing_panel.children = [self.local_file_panel]
self.local_file_button.on_click(self.load_local_file)

def list_dandiset_files_dropdown(self, args=None):
"""Populate dropdown with all files and text area with summary"""
self.dandi_summary.value = "Loading dandiset info..."
self.source_dandi_file_dropdown.options = []
dandiset_id = self.source_dandi_id.value.split("-")[0].strip()
self.source_dandi_file_dropdown.options = list_dandiset_files(dandiset_id=dandiset_id)

metadata = get_dandiset_metadata(dandiset_id=dandiset_id)
self.dandi_summary.value = "<style>p{word-wrap: break-word}</style> <p>" + metadata.description + "</p>"

def list_local_dir_files(self, args=None):
"""List NWB files in local dir"""
if Path(self.local_dir_path.value).is_dir():
all_files = [f.name for f in Path(self.local_dir_path.value).glob("*.nwb")]
self.local_dir_files.options = all_files
else:
print("Invalid local dir path")

def stream_dandiset_file(self, args=None):
"""Stream NWB file from DANDI"""
self.widgets_panel.children = [widgets.Label("loading...")]
Expand Down Expand Up @@ -290,19 +247,14 @@ def _stream_s3_file(self, s3_url):
self.nwbfile = self.io.read()
self.widgets_panel.children = [nwb2widget(self.nwbfile)]

def load_local_dir_file(self, args=None):
"""Load local NWB file"""
full_file_path = str(Path(self.local_dir_path.value) / self.local_dir_files.value)
io = NWBHDF5IO(full_file_path, mode="r", load_namespaces=True)
nwb = io.read()
self.widgets_panel.children = [nwb2widget(nwb)]

def load_local_file(self, args=None):
"""Load local NWB file"""
full_file_path = str(Path(self.local_file_path.value))
io = NWBHDF5IO(full_file_path, mode="r", load_namespaces=True)
nwb = io.read()
self.widgets_panel.children = [nwb2widget(nwb)]
full_file_path = str(Path(self.local_file_chooser.selected))
if self.io:
self.io.close()
self.io = NWBHDF5IO(full_file_path, mode="r", load_namespaces=True)
self.nwbfile = self.io.read()
self.widgets_panel.children = [nwb2widget(self.nwbfile)]

def process_dandiset(self, dandiset):
try:
Expand Down
2 changes: 1 addition & 1 deletion nwbwidgets/utils/dandi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def get_dandiset_metadata(dandiset_id: str):
def list_dandiset_files(dandiset_id: str):
with DandiAPIClient() as client:
dandiset = client.get_dandiset(dandiset_id=dandiset_id, version_id="draft")
return [i.dict().get("path") for i in dandiset.get_assets() if i.dict().get("path").endswith(".nwb")]
return sorted([i.dict().get("path") for i in dandiset.get_assets() if i.dict().get("path").endswith(".nwb")])


def get_file_url(dandiset_id: str, file_path: str):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ fsspec
requests
aiohttp
ipydatawidgets==4.3.2
ipyfilechooser==0.6.0
1 change: 0 additions & 1 deletion test/test_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def test_panel():
assert isinstance(panel, widgets.Widget)

# Change dropdown options for coverage
panel.source_options_radio.value = "Local dir"
panel.source_options_radio.value = "Local file"
panel.source_options_radio.value = "S3"
panel.source_options_radio.value = "DANDI"
Expand Down

0 comments on commit 8b8d6b4

Please sign in to comment.