Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MRG: Prototype of notebook viz (ipyvtk) #8503

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
50789de
Deploy basic version
GuillaumeFavelier Nov 10, 2020
cfd0678
Merge branch 'master' into proto/ipyvtk_backend
GuillaumeFavelier Nov 13, 2020
d7d2c5b
Merge branch 'master' into proto/ipyvtk_backend
GuillaumeFavelier Dec 16, 2020
4d9b63a
Update prototype [skip ci]
GuillaumeFavelier Dec 16, 2020
dcaff44
Update _Renderer [skip ci]
GuillaumeFavelier Dec 16, 2020
ec0d265
Update picking [skip ci]
GuillaumeFavelier Dec 16, 2020
9a22fb5
Fix disp
GuillaumeFavelier Dec 16, 2020
c254d79
Add shortcuts [skip ci]
GuillaumeFavelier Dec 17, 2020
a3fb818
Update tests
GuillaumeFavelier Dec 17, 2020
909909c
Add ipyvtk_simple
GuillaumeFavelier Dec 17, 2020
f317572
Fix style
GuillaumeFavelier Dec 17, 2020
08fdd85
Update tests
GuillaumeFavelier Dec 17, 2020
040bc3d
Refactor shortcuts
GuillaumeFavelier Dec 17, 2020
4cac2a0
Improve tests
GuillaumeFavelier Dec 17, 2020
a789b46
Remove cruft
GuillaumeFavelier Dec 17, 2020
fd3183b
Remove cruft
GuillaumeFavelier Dec 17, 2020
fd5c6d2
Reorder figures
GuillaumeFavelier Dec 17, 2020
ea8396e
Add tool bar
GuillaumeFavelier Dec 17, 2020
24852c4
Merge branch 'master' into proto/ipyvtk_backend
GuillaumeFavelier Dec 17, 2020
9a3d0fc
Reorder figures
GuillaumeFavelier Dec 17, 2020
137b7b1
Fix visibility
GuillaumeFavelier Dec 17, 2020
403176f
Merge branch 'master' into proto/ipyvtk_backend
GuillaumeFavelier Dec 18, 2020
3c31605
Improve coverage
GuillaumeFavelier Dec 18, 2020
a23e3c6
Fix ratio and layout
GuillaumeFavelier Dec 18, 2020
1cc4757
Merge branch 'master' into proto/ipyvtk_backend
GuillaumeFavelier Jan 4, 2021
b16738c
Update changes
GuillaumeFavelier Jan 4, 2021
195c91b
Do not import pyplot
GuillaumeFavelier Jan 5, 2021
ed680d3
Update mne/viz/_brain/_brain.py
GuillaumeFavelier Jan 5, 2021
57b505f
Use _add_action
GuillaumeFavelier Jan 6, 2021
4059082
Use icon + tooltip
GuillaumeFavelier Jan 6, 2021
af7f988
Fix qt func
GuillaumeFavelier Jan 6, 2021
b4b49f0
Merge branch 'master' into proto/ipyvtk_backend
GuillaumeFavelier Jan 6, 2021
a5e44e7
Reorder actions
GuillaumeFavelier Jan 6, 2021
0dcc7f6
Switch to icon + description
GuillaumeFavelier Jan 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Current (0.23.dev0)

Enhancements
~~~~~~~~~~~~
- None yet
- Update the ``notebook`` 3d backend to use ``ipyvtk_simple`` for a better integration within ``Jupyter`` (:gh:`8503` by `Guillaume Favelier`_)

Bugs
~~~~
Expand Down
227 changes: 141 additions & 86 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,10 @@ def __init__(self, subject_id, hemi, surf, title=None,
shape=shape,
fig=figure)

if _get_3d_backend() == "pyvista":
self.plotter = self._renderer.plotter
self.plotter = self._renderer.plotter
if self.notebook:
self.window = None
else:
self.window = self.plotter.app_window
self.window.signal_close.connect(self._clean)

Expand Down Expand Up @@ -501,11 +503,6 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
self.orientation = list(_lh_views_dict.keys())
self.default_smoothing_range = [0, 15]

# setup notebook
if self.notebook:
self._configure_notebook()
return

# Default configuration
self.playback = False
self.visibility = False
Expand Down Expand Up @@ -550,10 +547,15 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):

# Direct access parameters:
self._iren = self._renderer.plotter.iren
self.main_menu = self.plotter.main_menu
self.tool_bar = self.window.addToolBar("toolbar")
self.status_bar = self.window.statusBar()
self.interactor = self.plotter.interactor
self.tool_bar = None
if self.notebook:
self.main_menu = None
self.status_bar = None
self.interactor = None
else:
self.main_menu = self.plotter.main_menu
self.status_bar = self.window.statusBar()
self.interactor = self.plotter.interactor

# Derived parameters:
self.playback_speed = self.default_playback_speed_value
Expand Down Expand Up @@ -584,21 +586,24 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
self.separate_canvas = False
del show_traces

self._load_icons()
self._configure_time_label()
self._configure_sliders()
self._configure_scalar_bar()
self._configure_playback()
self._configure_menu()
self._configure_tool_bar()
self._configure_status_bar()
self._configure_shortcuts()
self._configure_picking()
self._configure_tool_bar()
if self.notebook:
self.show()
self._configure_trace_mode()

# show everything at the end
self.toggle_interface()
with self.ensure_minimum_sizes():
self.show()
if not self.notebook:
self._configure_playback()
self._configure_menu()
self._configure_status_bar()

# show everything at the end
with self.ensure_minimum_sizes():
self.show()

@safe_event
def _clean(self):
Expand Down Expand Up @@ -677,10 +682,13 @@ def toggle_interface(self, value=None):
self.visibility = value

# update tool bar icon
if self.visibility:
self.actions["visibility"].setIcon(self.icons["visibility_on"])
else:
self.actions["visibility"].setIcon(self.icons["visibility_off"])
if not self.notebook:
if self.visibility:
self.actions["visibility"].setIcon(
self.icons["visibility_on"])
else:
self.actions["visibility"].setIcon(
self.icons["visibility_off"])

# manage sliders
for slider in self.plotter.slider_widgets:
Expand Down Expand Up @@ -810,10 +818,6 @@ def _set_slider_style(self):
)
slider_rep.GetCapProperty().SetOpacity(0)

def _configure_notebook(self):
from ._notebook import _NotebookInteractor
self._renderer.figure.display = _NotebookInteractor(self)

def _configure_time_label(self):
self.time_actor = self._data.get('time_actor')
if self.time_actor is not None:
Expand Down Expand Up @@ -998,19 +1002,24 @@ def _configure_playback(self):
self.plotter.add_callback(self._play, self.refresh_rate_ms)

def _configure_mplcanvas(self):
win = self.plotter.app_window
dpi = win.windowHandle().screen().logicalDotsPerInch()
ratio = (1 - self.interactor_fraction) / self.interactor_fraction
w = self.interactor.geometry().width()
h = self.interactor.geometry().height() / ratio
if self.notebook:
dpi = 96
w, h = self.plotter.window_size
else:
dpi = self.window.windowHandle().screen().logicalDotsPerInch()
w = self.interactor.geometry().width()
h = self.interactor.geometry().height()
h /= ratio
# Get the fractional components for the brain and mpl
self.mpl_canvas = MplCanvas(self, w / dpi, h / dpi, dpi)
self.mpl_canvas = MplCanvas(self, w / dpi, h / dpi, dpi,
self.notebook)
xlim = [np.min(self._data['time']),
np.max(self._data['time'])]
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
self.mpl_canvas.axes.set(xlim=xlim)
if not self.separate_canvas:
if not self.notebook and not self.separate_canvas:
from PyQt5.QtWidgets import QSplitter
from PyQt5.QtCore import Qt
canvas = self.mpl_canvas.canvas
Expand Down Expand Up @@ -1106,10 +1115,13 @@ def _configure_picking(self):
def _configure_trace_mode(self):
from ...source_estimate import _get_allowed_label_modes
from ...label import _read_annot_cands
from PyQt5.QtWidgets import QComboBox, QLabel
if not self.show_traces:
return

if self.notebook:
self._configure_vertex_time_course()
return

# do not show trace mode for volumes
if (self._data.get('src', None) is not None and
self._data['src'].kind == 'volume'):
Expand All @@ -1131,6 +1143,7 @@ def _set_annot(annot):
self._configure_label_time_course()
self._update()

from PyQt5.QtWidgets import QComboBox, QLabel
dir_name = op.join(self._subjects_dir, self._subject_id, 'label')
cands = _read_annot_cands(dir_name)
self.tool_bar.addSeparator()
Expand Down Expand Up @@ -1201,60 +1214,109 @@ def _load_icons(self):
def _save_movie_noname(self):
return self.save_movie(None)

def _screenshot(self):
if not self.notebook:
self.plotter._qt_screenshot()

def _initialize_actions(self):
if not self.notebook:
self._load_icons()
self.tool_bar = self.window.addToolBar("toolbar")

def _add_action(self, name, desc, func, icon_name, qt_icon_name=None,
notebook=True):
if self.notebook:
if not notebook:
return
from ipywidgets import Button
self.actions[name] = Button(description=desc, icon=icon_name)
self.actions[name].on_click(lambda x: func())
else:
qt_icon_name = name if qt_icon_name is None else qt_icon_name
self.actions[name] = self.tool_bar.addAction(
self.icons[qt_icon_name],
desc,
func,
)

def _configure_tool_bar(self):
self.actions["screenshot"] = self.tool_bar.addAction(
self.icons["screenshot"],
"Take a screenshot",
self.plotter._qt_screenshot
self._initialize_actions()
self._add_action(
name="screenshot",
desc="Take a screenshot",
func=self._screenshot,
icon_name=None,
notebook=False,
)
self.actions["movie"] = self.tool_bar.addAction(
self.icons["movie"],
"Save movie...",
self._save_movie_noname,
self._add_action(
name="movie",
desc="Save movie...",
func=self._save_movie_noname,
icon_name=None,
notebook=False,
)
self.actions["visibility"] = self.tool_bar.addAction(
self.icons["visibility_on"],
"Toggle Visibility",
self.toggle_interface
self._add_action(
name="visibility",
desc="Toggle Visibility",
func=self.toggle_interface,
icon_name="eye",
qt_icon_name="visibility_on",
)
self.actions["play"] = self.tool_bar.addAction(
self.icons["play"],
"Play/Pause",
self.toggle_playback
self._add_action(
name="play",
desc="Play/Pause",
func=self.toggle_playback,
icon_name=None,
notebook=False,
)
self.actions["reset"] = self.tool_bar.addAction(
self.icons["reset"],
"Reset",
self.reset
self._add_action(
name="reset",
desc="Reset",
func=self.reset,
icon_name="history",
)
self.actions["scale"] = self.tool_bar.addAction(
self.icons["scale"],
"Auto-Scale",
self.apply_auto_scaling
self._add_action(
name="scale",
desc="Auto-Scale",
func=self.apply_auto_scaling,
icon_name="magic",
)
self.actions["restore"] = self.tool_bar.addAction(
self.icons["restore"],
"Restore scaling",
self.restore_user_scaling
self._add_action(
name="restore",
desc="Restore scaling",
func=self.restore_user_scaling,
icon_name="reply",
)
self.actions["clear"] = self.tool_bar.addAction(
self.icons["clear"],
"Clear traces",
self.clear_glyphs
self._add_action(
name="clear",
desc="Clear traces",
func=self.clear_glyphs,
icon_name="trash",
)
self.actions["help"] = self.tool_bar.addAction(
self.icons["help"],
"Help",
self.help
self._add_action(
name="help",
desc="Help",
func=self.help,
icon_name=None,
notebook=False,
)

self.actions["movie"].setShortcut("ctrl+shift+s")
self.actions["visibility"].setShortcut("i")
self.actions["play"].setShortcut(" ")
self.actions["scale"].setShortcut("s")
self.actions["restore"].setShortcut("r")
self.actions["clear"].setShortcut("c")
self.actions["help"].setShortcut("?")
if self.notebook:
from IPython import display
from ipywidgets import HBox
self.tool_bar = HBox(tuple(self.actions.values()))
display.display(self.tool_bar)
else:
# Qt shortcuts
self.actions["movie"].setShortcut("ctrl+shift+s")
self.actions["play"].setShortcut(" ")
self.actions["help"].setShortcut("?")

def _configure_shortcuts(self):
self.plotter.add_key_event("i", self.toggle_interface)
self.plotter.add_key_event("s", self.apply_auto_scaling)
self.plotter.add_key_event("r", self.restore_user_scaling)
self.plotter.add_key_event("c", self.clear_glyphs)

def _configure_menu(self):
# remove default picking menu
Expand Down Expand Up @@ -3123,13 +3185,6 @@ def _iter_time(self, time_idx, callback):
# Restore original time index
func(current_time_idx)

def _show(self):
"""Request rendering of the window."""
try:
return self._renderer.show()
except RuntimeError:
logger.info("No active/running renderer available.")

def _check_stc(self, hemi, array, vertices):
from ...source_estimate import (
_BaseSourceEstimate, _BaseSurfaceSourceEstimate,
Expand Down Expand Up @@ -3220,7 +3275,7 @@ def _update(self):
from ..backends import renderer
if renderer.get_3d_backend() in ['pyvista', 'notebook']:
if self.notebook and self._renderer.figure.display is not None:
self._renderer.figure.display.update()
self._renderer.figure.display.update_canvas()
else:
self._renderer.plotter.update()

Expand Down
Loading