From 50789de2dbb3adb850098a7e7aa6e67820cd0d8c Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 10 Nov 2020 16:54:21 +0100 Subject: [PATCH 01/28] Deploy basic version --- mne/viz/_brain/_brain.py | 14 ++++++++++++-- mne/viz/_brain/_notebook.py | 8 ++------ mne/viz/backends/_notebook.py | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5ff8a2207f8..013a001f4dd 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -648,8 +648,18 @@ def _set_slider_style(self): slider_rep.GetCapProperty().SetOpacity(0) def _configure_notebook(self): - from ._notebook import _NotebookInteractor - self._renderer.figure.display = _NotebookInteractor(self) + from IPython import display + from ipywidgets import VBox + from mne.viz._brain._notebook import _NotebookInteractor + self.disp = self._renderer.plotter.show(use_ipyvtk=True, + return_viewer=True) + nint = _NotebookInteractor(self) + nint.controllers = dict() + nint.sliders = dict() + nint.configure_controllers() + controllers = VBox(list(nint.controllers.values())) + layout = VBox([controllers, self.disp]) + display.display(layout) def _configure_time_label(self): self.time_actor = self._data.get('time_actor') diff --git a/mne/viz/_brain/_notebook.py b/mne/viz/_brain/_notebook.py index 801ba240c07..b3dc6e8717e 100644 --- a/mne/viz/_brain/_notebook.py +++ b/mne/viz/_brain/_notebook.py @@ -2,19 +2,15 @@ # # License: Simplified BSD -from ..backends._notebook \ - import _NotebookInteractor as _PyVistaNotebookInteractor - -class _NotebookInteractor(_PyVistaNotebookInteractor): +class _NotebookInteractor(object): def __init__(self, brain): self.brain = brain - super().__init__(self.brain._renderer) + self.plotter = brain._renderer.plotter def configure_controllers(self): from ipywidgets import (IntSlider, interactive, Play, VBox, HBox, Label, jslink) - super().configure_controllers() # orientation self.controllers["orientation"] = interactive( self.set_orientation, diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 7a8702e7b47..1350f57f14c 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -19,7 +19,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def show(self): - self.figure.display = _NotebookInteractor(self) + self.disp = self.plotter.show(use_ipyvtk=True, return_viewer=True) return self.scene() From 4d9b63a0b1914ba2cf523a1d17d5187870b6f925 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 16 Dec 2020 10:42:34 +0100 Subject: [PATCH 02/28] Update prototype [skip ci] --- mne/viz/_brain/_brain.py | 83 ++++++++++++++++++------------------- mne/viz/_brain/_notebook.py | 63 ---------------------------- 2 files changed, 40 insertions(+), 106 deletions(-) delete mode 100644 mne/viz/_brain/_notebook.py diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 8cedf6c0034..709e6ddd3e3 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -424,10 +424,13 @@ def __init__(self, subject_id, hemi, surf, title=None, shape=shape, fig=figure) - if _get_3d_backend() == "pyvista": + self.plotter = None + self.window = None + if _get_3d_backend() in ("pyvista", "notebook"): self.plotter = self._renderer.plotter - self.window = self.plotter.app_window - self.window.signal_close.connect(self._clean) + if hasattr(self.plotter, "app_window"): + self.window = self.plotter.app_window + self.window.signal_close.connect(self._clean) for h in self._hemis: # Initialize a Surface object as the geometry @@ -494,11 +497,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 @@ -527,10 +525,16 @@ 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 + if self.window is not None: + 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 + else: + self.main_menu = None + self.tool_bar = None + self.status_bar = None + self.interactor = None # Derived parameters: self.playback_speed = self.default_playback_speed_value @@ -559,11 +563,12 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_time_label() self._configure_sliders() self._configure_scalar_bar() - self._configure_playback() - self._configure_point_picking() - self._configure_menu() - self._configure_tool_bar() - self._configure_status_bar() + if self.window is not None: + self._configure_playback() + self._configure_point_picking() + self._configure_menu() + self._configure_tool_bar() + self._configure_status_bar() # show everything at the end self.toggle_interface() @@ -646,10 +651,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 self.window is not None: + 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: @@ -779,20 +787,6 @@ def _set_slider_style(self): ) slider_rep.GetCapProperty().SetOpacity(0) - def _configure_notebook(self): - from IPython import display - from ipywidgets import VBox - from mne.viz._brain._notebook import _NotebookInteractor - self.disp = self._renderer.plotter.show(use_ipyvtk=True, - return_viewer=True) - nint = _NotebookInteractor(self) - nint.controllers = dict() - nint.sliders = dict() - nint.configure_controllers() - controllers = VBox(list(nint.controllers.values())) - layout = VBox([controllers, self.disp]) - display.display(layout) - def _configure_time_label(self): self.time_actor = self._data.get('time_actor') if self.time_actor is not None: @@ -2271,7 +2265,17 @@ def close(self): def show(self): """Display the window.""" - self._renderer.show() + if self.notebook: + from IPython import display + self.disp = self._renderer.plotter.show(use_ipyvtk=True, + return_viewer=True) + display.display(self.disp) + else: + # Request rendering of the window + try: + return self._renderer.show() + except RuntimeError: + logger.info("No active/running renderer available.") def show_view(self, view=None, roll=None, distance=None, row=0, col=0, hemi=None): @@ -2938,13 +2942,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, diff --git a/mne/viz/_brain/_notebook.py b/mne/viz/_brain/_notebook.py deleted file mode 100644 index b3dc6e8717e..00000000000 --- a/mne/viz/_brain/_notebook.py +++ /dev/null @@ -1,63 +0,0 @@ -# Authors: Guillaume Favelier -# -# License: Simplified BSD - - -class _NotebookInteractor(object): - def __init__(self, brain): - self.brain = brain - self.plotter = brain._renderer.plotter - - def configure_controllers(self): - from ipywidgets import (IntSlider, interactive, Play, VBox, - HBox, Label, jslink) - # orientation - self.controllers["orientation"] = interactive( - self.set_orientation, - orientation=self.brain.orientation, - ) - # smoothing - self.sliders["smoothing"] = IntSlider( - value=self.brain._data['smoothing_steps'], - min=self.brain.default_smoothing_range[0], - max=self.brain.default_smoothing_range[1], - continuous_update=False - ) - self.controllers["smoothing"] = VBox([ - Label(value='Smoothing steps'), - interactive( - self.brain.set_data_smoothing, - n_steps=self.sliders["smoothing"] - ) - ]) - # time slider - max_time = len(self.brain._data['time']) - 1 - if max_time >= 1: - time_player = Play( - value=self.brain._data['time_idx'], - min=0, - max=max_time, - continuous_update=False - ) - time_slider = IntSlider( - min=0, - max=max_time, - ) - jslink((time_player, 'value'), (time_slider, 'value')) - time_slider.observe(self.set_time_point, 'value') - self.controllers["time"] = VBox([ - HBox([ - Label(value='Select time point'), - time_player, - ]), - time_slider, - ]) - self.sliders["time"] = time_slider - - def set_orientation(self, orientation): - row, col = self.plotter.index_to_loc( - self.plotter._active_renderer_index) - self.brain.show_view(orientation, row=row, col=col) - - def set_time_point(self, data): - self.brain.set_time_point(data['new']) From dcaff4453e1609652651d02cb348a9d02c485e62 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 16 Dec 2020 14:06:54 +0100 Subject: [PATCH 03/28] Update _Renderer [skip ci] --- mne/viz/_brain/_brain.py | 10 +-- mne/viz/backends/_notebook.py | 149 +--------------------------------- 2 files changed, 6 insertions(+), 153 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 709e6ddd3e3..0623fab8e5d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -572,7 +572,10 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): # show everything at the end self.toggle_interface() - with self.ensure_minimum_sizes(): + if self.window is not None: + with self.ensure_minimum_sizes(): + self.show() + else: self.show() @safe_event @@ -2266,10 +2269,7 @@ def close(self): def show(self): """Display the window.""" if self.notebook: - from IPython import display - self.disp = self._renderer.plotter.show(use_ipyvtk=True, - return_viewer=True) - display.display(self.disp) + self.disp = self._renderer.plotter.show(use_ipyvtk=True) else: # Request rendering of the window try: diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index be9166a9572..d0af3507f8d 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -2,8 +2,6 @@ # # License: Simplified BSD -import matplotlib.pyplot as plt -from contextlib import contextmanager from ...fixes import nullcontext from ._pyvista import _Renderer as _PyVistaRenderer from ._pyvista import \ @@ -12,157 +10,12 @@ class _Renderer(_PyVistaRenderer): def __init__(self, *args, **kwargs): - from IPython import get_ipython - ipython = get_ipython() - ipython.magic('matplotlib widget') kwargs["notebook"] = True super().__init__(*args, **kwargs) def show(self): - self.disp = self.plotter.show(use_ipyvtk=True, return_viewer=True) + self.disp = self.plotter.show(use_ipyvtk=True) return self.scene() -class _NotebookInteractor(object): - def __init__(self, renderer): - from IPython import display - from ipywidgets import HBox, VBox - self.dpi = 90 - self.sliders = dict() - self.controllers = dict() - self.renderer = renderer - self.plotter = self.renderer.plotter - with self.disabled_interactivity(): - self.fig, self.dh = self.screenshot() - self.configure_controllers() - controllers = VBox(list(self.controllers.values())) - layout = HBox([self.fig.canvas, controllers]) - display.display(layout) - - @contextmanager - def disabled_interactivity(self): - state = plt.isinteractive() - plt.ioff() - try: - yield - finally: - if state: - plt.ion() - else: - plt.ioff() - - def screenshot(self): - width, height = self.renderer.figure.store['window_size'] - - fig = plt.figure() - fig.figsize = (width / self.dpi, height / self.dpi) - fig.dpi = self.dpi - fig.canvas.toolbar_visible = False - fig.canvas.header_visible = False - fig.canvas.resizable = False - fig.canvas.callbacks.callbacks.clear() - ax = plt.Axes(fig, [0., 0., 1., 1.]) - ax.set_axis_off() - fig.add_axes(ax) - - dh = ax.imshow(self.plotter.screenshot()) - return fig, dh - - def update(self): - self.plotter.render() - self.dh.set_data(self.plotter.screenshot()) - self.fig.canvas.draw() - - def configure_controllers(self): - from ipywidgets import (interactive, Label, VBox, FloatSlider, - IntSlider, Checkbox) - # continuous update - self.continuous_update_button = Checkbox( - value=False, - description='Continuous update', - disabled=False, - indent=False, - ) - self.controllers["continuous_update"] = interactive( - self.set_continuous_update, - value=self.continuous_update_button - ) - # subplot - number_of_plots = len(self.plotter.renderers) - if number_of_plots > 1: - self.sliders["subplot"] = IntSlider( - value=number_of_plots - 1, - min=0, - max=number_of_plots - 1, - step=1, - continuous_update=False - ) - self.controllers["subplot"] = VBox([ - Label(value='Select the subplot'), - interactive( - self.set_subplot, - index=self.sliders["subplot"], - ) - ]) - # azimuth - default_azimuth = self.plotter.renderer._azimuth - self.sliders["azimuth"] = FloatSlider( - value=default_azimuth, - min=-180., - max=180., - step=10., - continuous_update=False - ) - # elevation - default_elevation = self.plotter.renderer._elevation - self.sliders["elevation"] = FloatSlider( - value=default_elevation, - min=-180., - max=180., - step=10., - continuous_update=False - ) - # distance - eps = 1e-5 - default_distance = self.plotter.renderer._distance - self.sliders["distance"] = FloatSlider( - value=default_distance, - min=eps, - max=2. * default_distance - eps, - step=default_distance / 10., - continuous_update=False - ) - # camera - self.controllers["camera"] = VBox([ - Label(value='Camera settings'), - interactive( - self.set_camera, - azimuth=self.sliders["azimuth"], - elevation=self.sliders["elevation"], - distance=self.sliders["distance"], - ) - ]) - - def set_camera(self, azimuth, elevation, distance): - focalpoint = self.plotter.camera.GetFocalPoint() - self.renderer.set_camera(azimuth, elevation, - distance, focalpoint) - self.update() - - def set_subplot(self, index): - row, col = self.plotter.index_to_loc(index) - self.renderer.subplot(row, col) - figure = self.renderer.figure - default_azimuth = figure.plotter.renderer._azimuth - default_elevation = figure.plotter.renderer._elevation - default_distance = figure.plotter.renderer._distance - self.sliders["azimuth"].value = default_azimuth - self.sliders["elevation"].value = default_elevation - self.sliders["distance"].value = default_distance - - def set_continuous_update(self, value): - for slider in self.sliders.values(): - slider.continuous_update = value - - _testing_context = nullcontext From ec0d2657b344e93883eb3287ab24c90472b322a5 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 16 Dec 2020 15:48:37 +0100 Subject: [PATCH 04/28] Update picking [skip ci] --- mne/viz/_brain/_brain.py | 55 +++++++++++++++++++------------------ mne/viz/_brain/mplcanvas.py | 26 +++++++++++------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 0623fab8e5d..40e811bc908 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -525,16 +525,16 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): # Direct access parameters: self._iren = self._renderer.plotter.iren - if self.window is not None: - 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 - else: + if self.notebook: self.main_menu = None self.tool_bar = None self.status_bar = None self.interactor = None + else: + 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 # Derived parameters: self.playback_speed = self.default_playback_speed_value @@ -563,20 +563,20 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_time_label() self._configure_sliders() self._configure_scalar_bar() - if self.window is not None: + self._configure_point_picking() + if not self.notebook: self._configure_playback() - self._configure_point_picking() self._configure_menu() self._configure_tool_bar() self._configure_status_bar() # show everything at the end self.toggle_interface() - if self.window is not None: + if self.notebook: + self.show() + else: with self.ensure_minimum_sizes(): self.show() - else: - self.show() @safe_event def _clean(self): @@ -654,7 +654,7 @@ def toggle_interface(self, value=None): self.visibility = value # update tool bar icon - if self.window is not None: + if not self.notebook: if self.visibility: self.actions["visibility"].setIcon( self.icons["visibility_on"]) @@ -979,19 +979,23 @@ def _configure_point_picking(self): from ..backends._pyvista import _update_picking_callback # use a matplotlib canvas self.color_cycle = _ReuseCycle(_get_color_list()) - 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 = 90 + w, h = self.plotter.window_size + else: + dpi = self.window.windowHandle().screen().logicalDotsPerInch() + ratio = (1 - self.interactor_fraction) / self.interactor_fraction + w = self.interactor.geometry().width() + h = self.interactor.geometry().height() / 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 @@ -2268,14 +2272,11 @@ def close(self): def show(self): """Display the window.""" - if self.notebook: - self.disp = self._renderer.plotter.show(use_ipyvtk=True) - else: - # Request rendering of the window - try: - return self._renderer.show() - except RuntimeError: - logger.info("No active/running renderer available.") + # Request rendering of the window + try: + return self._renderer.show() + except RuntimeError: + logger.info("No active/running renderer available.") def show_view(self, view=None, roll=None, distance=None, row=0, col=0, hemi=None): diff --git a/mne/viz/_brain/mplcanvas.py b/mne/viz/_brain/mplcanvas.py index 23b9f4d7295..378fb766db9 100644 --- a/mne/viz/_brain/mplcanvas.py +++ b/mne/viz/_brain/mplcanvas.py @@ -11,8 +11,9 @@ class MplCanvas(object): """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" - def __init__(self, brain, width, height, dpi): + def __init__(self, brain, width, height, dpi, notebook=False): from PyQt5 import QtWidgets + import matplotlib.pyplot as plt from matplotlib import rc_context from matplotlib.figure import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg @@ -30,16 +31,21 @@ def __init__(self, brain, width, height, dpi): pass with context: self.fig = Figure(figsize=(width, height), dpi=dpi) - self.canvas = FigureCanvasQTAgg(self.fig) - self.axes = self.fig.add_subplot(111) + self.notebook = notebook + if self.notebook: + self.fig, self.axes = plt.subplots() + self.canvas = plt.gcf().canvas + else: + self.canvas = FigureCanvasQTAgg(self.fig) + self.axes = self.fig.add_subplot(111) + self.canvas.setParent(parent) + FigureCanvasQTAgg.setSizePolicy( + self.canvas, + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + FigureCanvasQTAgg.updateGeometry(self.canvas) self.axes.set(xlabel='Time (sec)', ylabel='Activation (AU)') - self.canvas.setParent(parent) - FigureCanvasQTAgg.setSizePolicy( - self.canvas, - QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding - ) - FigureCanvasQTAgg.updateGeometry(self.canvas) self.brain = brain self.time_func = brain.callbacks["time"] for event in ('button_press', 'motion_notify') + extra_events: From 9a22fb50b0b2744cfda2c91cb5f7785099c20baa Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 16 Dec 2020 16:11:50 +0100 Subject: [PATCH 05/28] Fix disp --- mne/viz/_brain/_brain.py | 6 +++--- mne/viz/_brain/mplcanvas.py | 17 +++++++++-------- mne/viz/backends/_notebook.py | 5 ++++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 40e811bc908..5635a9a3b53 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -980,7 +980,7 @@ def _configure_point_picking(self): # use a matplotlib canvas self.color_cycle = _ReuseCycle(_get_color_list()) if self.notebook: - dpi = 90 + dpi = 100 w, h = self.plotter.window_size else: dpi = self.window.windowHandle().screen().logicalDotsPerInch() @@ -3032,8 +3032,8 @@ def enable_depth_peeling(self): 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() + if self.notebook and self._renderer.disp is not None: + self._renderer.disp.update_canvas() else: self._renderer.plotter.update() diff --git a/mne/viz/_brain/mplcanvas.py b/mne/viz/_brain/mplcanvas.py index 378fb766db9..8d4e944f275 100644 --- a/mne/viz/_brain/mplcanvas.py +++ b/mne/viz/_brain/mplcanvas.py @@ -24,18 +24,19 @@ def __init__(self, brain, width, height, dpi, notebook=False): # prefer constrained layout here but live with tight_layout otherwise context = nullcontext extra_events = ('resize',) - try: - context = rc_context({'figure.constrained_layout.use': True}) - extra_events = () - except KeyError: - pass - with context: - self.fig = Figure(figsize=(width, height), dpi=dpi) self.notebook = notebook if self.notebook: - self.fig, self.axes = plt.subplots() + self.fig, self.axes = plt.subplots(figsize=(width, height), + dpi=dpi) self.canvas = plt.gcf().canvas else: + try: + context = rc_context({'figure.constrained_layout.use': True}) + extra_events = () + except KeyError: + pass + with context: + self.fig = Figure(figsize=(width, height), dpi=dpi) self.canvas = FigureCanvasQTAgg(self.fig) self.axes = self.fig.add_subplot(111) self.canvas.setParent(parent) diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index d0af3507f8d..814ae2f745a 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -10,11 +10,14 @@ class _Renderer(_PyVistaRenderer): def __init__(self, *args, **kwargs): + self.disp = None kwargs["notebook"] = True super().__init__(*args, **kwargs) def show(self): - self.disp = self.plotter.show(use_ipyvtk=True) + from IPython.display import display + self.disp = self.plotter.show(use_ipyvtk=True, return_viewer=True) + display(self.disp) return self.scene() From c254d79bffb5c2ae66895d0d89bc33b5830391b8 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 10:48:04 +0100 Subject: [PATCH 06/28] Add shortcuts [skip ci] --- mne/viz/_brain/_brain.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5635a9a3b53..755cce4f5a0 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -564,7 +564,9 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_sliders() self._configure_scalar_bar() self._configure_point_picking() - if not self.notebook: + if self.notebook: + self._configure_shortcuts() + else: self._configure_playback() self._configure_menu() self._configure_tool_bar() @@ -1151,6 +1153,13 @@ def _configure_tool_bar(self): self.actions["clear"].setShortcut("c") self.actions["help"].setShortcut("?") + def _configure_shortcuts(self): + self.plotter.add_key_event("i", self.toggle_interface) + self.plotter.add_key_event(" ", self.toggle_playback) + 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_points) + def _configure_menu(self): # remove default picking menu to_remove = list() From a3fb818cabb0ed74745d33b73ea191e9c7c928b5 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 11:23:43 +0100 Subject: [PATCH 07/28] Update tests --- mne/viz/_brain/mplcanvas.py | 3 ++- mne/viz/_brain/tests/test.ipynb | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/mplcanvas.py b/mne/viz/_brain/mplcanvas.py index 8d4e944f275..a00f6aa8a38 100644 --- a/mne/viz/_brain/mplcanvas.py +++ b/mne/viz/_brain/mplcanvas.py @@ -93,7 +93,8 @@ def set_color(self, bg_color, fg_color): def show(self): """Show the canvas.""" - self.canvas.show() + if hasattr(self.canvas, "show"): + self.canvas.show() def close(self): """Close the canvas.""" diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 0e7dbe7ffbd..2bea7db3e78 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -46,8 +46,7 @@ " hemi='split')\n", " assert isinstance(brain, brain_class)\n", " assert brain.notebook\n", - " interactor = brain._renderer.figure.display\n", - " interactor.set_time_point({'new': 0})\n", + " assert brain._renderer.disp is not None\n", " brain.close()" ] }, From 909909c55bd6768fb969f7f6cec1aa2bd4c7f19d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 11:29:41 +0100 Subject: [PATCH 08/28] Add ipyvtk_simple --- server_environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/server_environment.yml b/server_environment.yml index 8530c5dd767..86c3fda05b6 100644 --- a/server_environment.yml +++ b/server_environment.yml @@ -23,4 +23,5 @@ dependencies: - jupyter - ipympl - ipywidgets + - ipyvtk_simple - jupyter_client!=6.1.5 From f317572a84406e3ee3f3221eaceeca254d512138 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 11:29:58 +0100 Subject: [PATCH 09/28] Fix style --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 755cce4f5a0..fcda7be163b 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -2283,7 +2283,7 @@ def show(self): """Display the window.""" # Request rendering of the window try: - return self._renderer.show() + self._renderer.show() except RuntimeError: logger.info("No active/running renderer available.") From 08fdd85e93afe32d584478b065c8cd20e36c1017 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 11:52:23 +0100 Subject: [PATCH 10/28] Update tests --- mne/viz/_brain/_brain.py | 2 +- mne/viz/_brain/mplcanvas.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index fcda7be163b..de13e98b26d 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -559,7 +559,6 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): del show_traces self._spheres = list() - self._load_icons() self._configure_time_label() self._configure_sliders() self._configure_scalar_bar() @@ -567,6 +566,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): if self.notebook: self._configure_shortcuts() else: + self._load_icons() self._configure_playback() self._configure_menu() self._configure_tool_bar() diff --git a/mne/viz/_brain/mplcanvas.py b/mne/viz/_brain/mplcanvas.py index a00f6aa8a38..798d476d7c7 100644 --- a/mne/viz/_brain/mplcanvas.py +++ b/mne/viz/_brain/mplcanvas.py @@ -12,11 +12,9 @@ class MplCanvas(object): """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" def __init__(self, brain, width, height, dpi, notebook=False): - from PyQt5 import QtWidgets import matplotlib.pyplot as plt from matplotlib import rc_context from matplotlib.figure import Figure - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg if brain.separate_canvas: parent = None else: @@ -30,6 +28,8 @@ def __init__(self, brain, width, height, dpi, notebook=False): dpi=dpi) self.canvas = plt.gcf().canvas else: + from PyQt5 import QtWidgets + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg try: context = rc_context({'figure.constrained_layout.use': True}) extra_events = () From 040bc3de3a49fc42b33d797a472d0fc3080e606d Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 14:54:50 +0100 Subject: [PATCH 11/28] Refactor shortcuts --- mne/viz/_brain/_brain.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index de13e98b26d..5be79598e18 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -563,9 +563,8 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_sliders() self._configure_scalar_bar() self._configure_point_picking() - if self.notebook: - self._configure_shortcuts() - else: + self._configure_shortcuts() + if not self.notebook: self._load_icons() self._configure_playback() self._configure_menu() @@ -1145,17 +1144,13 @@ def _configure_tool_bar(self): self.help ) + # Qt shortcuts 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("?") def _configure_shortcuts(self): self.plotter.add_key_event("i", self.toggle_interface) - self.plotter.add_key_event(" ", self.toggle_playback) 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_points) From 4cac2a0f1a86d3f086de4142fae242d250a5a174 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 15:09:58 +0100 Subject: [PATCH 12/28] Improve tests --- mne/viz/_brain/_brain.py | 4 ++-- mne/viz/_brain/tests/test.ipynb | 17 +++++++++++------ mne/viz/backends/_notebook.py | 5 +++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 5be79598e18..2eaa75d3918 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -3036,8 +3036,8 @@ def enable_depth_peeling(self): def _update(self): from ..backends import renderer if renderer.get_3d_backend() in ['pyvista', 'notebook']: - if self.notebook and self._renderer.disp is not None: - self._renderer.disp.update_canvas() + if self.notebook and self._renderer.figure.display is not None: + self._renderer.figure.display.update_canvas() else: self._renderer.plotter.update() diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index 2bea7db3e78..fb2e7cfd317 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -15,9 +15,10 @@ "trans = data_path + '/MEG/sample/sample_audvis_trunc-trans.fif'\n", "info = mne.io.read_info(raw_fname)\n", "mne.viz.set_3d_backend('notebook')\n", - "mne.viz.plot_alignment(info, trans, subject=subject, dig=True,\n", - " meg=['helmet', 'sensors'], subjects_dir=subjects_dir,\n", - " surfaces=['head-dense'])" + "fig = mne.viz.plot_alignment(info, trans, subject=subject, dig=True,\n", + " meg=['helmet', 'sensors'], subjects_dir=subjects_dir,\n", + " surfaces=['head-dense'])\n", + "assert fig.display is not None" ] }, { @@ -46,7 +47,7 @@ " hemi='split')\n", " assert isinstance(brain, brain_class)\n", " assert brain.notebook\n", - " assert brain._renderer.disp is not None\n", + " assert brain._renderer.figure.display is not None\n", " brain.close()" ] }, @@ -58,9 +59,13 @@ "source": [ "import mne\n", "mne.viz.set_3d_backend('notebook')\n", - "fig = mne.viz.create_3d_figure(size=(100, 100))\n", + "rend = mne.viz.create_3d_figure(size=(100, 100), scene=False)\n", + "fig = rend.scene()\n", "mne.viz.set_3d_title(fig, 'Notebook testing')\n", - "mne.viz.set_3d_view(fig, 200, 70, focalpoint=[0, 0, 0])" + "mne.viz.set_3d_view(fig, 200, 70, focalpoint=[0, 0, 0])\n", + "assert fig.display is None\n", + "rend.show()\n", + "assert fig.display is not None" ] } ], diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 814ae2f745a..c6ceb5562a2 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -16,8 +16,9 @@ def __init__(self, *args, **kwargs): def show(self): from IPython.display import display - self.disp = self.plotter.show(use_ipyvtk=True, return_viewer=True) - display(self.disp) + self.figure.display = self.plotter.show(use_ipyvtk=True, + return_viewer=True) + display(self.figure.display) return self.scene() From a789b46638d3bf90116bc87d3703bc83275abb64 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 15:17:42 +0100 Subject: [PATCH 13/28] Remove cruft --- mne/viz/backends/_notebook.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index c6ceb5562a2..43aa11292e8 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -10,7 +10,6 @@ class _Renderer(_PyVistaRenderer): def __init__(self, *args, **kwargs): - self.disp = None kwargs["notebook"] = True super().__init__(*args, **kwargs) From fd3183b1dcd22f1cc423488effbb5e60eb919668 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 15:26:06 +0100 Subject: [PATCH 14/28] Remove cruft --- mne/viz/_brain/_brain.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 2eaa75d3918..d8cb47b3f27 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -424,13 +424,12 @@ def __init__(self, subject_id, hemi, surf, title=None, shape=shape, fig=figure) - self.plotter = None - self.window = None - if _get_3d_backend() in ("pyvista", "notebook"): - self.plotter = self._renderer.plotter - if hasattr(self.plotter, "app_window"): - self.window = self.plotter.app_window - self.window.signal_close.connect(self._clean) + 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) for h in self._hemis: # Initialize a Surface object as the geometry @@ -2276,11 +2275,7 @@ def close(self): def show(self): """Display the window.""" - # Request rendering of the window - try: - self._renderer.show() - except RuntimeError: - logger.info("No active/running renderer available.") + self._renderer.show() def show_view(self, view=None, roll=None, distance=None, row=0, col=0, hemi=None): From fd5c6d20cf1a69c0aba534ab1ae53a272cea6e2e Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 15:43:24 +0100 Subject: [PATCH 15/28] Reorder figures --- mne/viz/_brain/_brain.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index d8cb47b3f27..073c0bab8e1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -492,6 +492,8 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): """ if self.time_viewer: return + if self.notebook: + self.show() self.time_viewer = time_viewer self.orientation = list(_lh_views_dict.keys()) self.default_smoothing_range = [0, 15] @@ -570,11 +572,9 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_tool_bar() self._configure_status_bar() - # show everything at the end - self.toggle_interface() - if self.notebook: - self.show() - else: + if not self.notebook: + # show everything at the end + self.toggle_interface() with self.ensure_minimum_sizes(): self.show() From ea8396ef37225178ef9c398bd6fd03a667ac6f0e Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 16:41:43 +0100 Subject: [PATCH 16/28] Add tool bar --- mne/viz/_brain/_brain.py | 136 ++++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 58 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 073c0bab8e1..a3d78d96991 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -492,8 +492,6 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): """ if self.time_viewer: return - if self.notebook: - self.show() self.time_viewer = time_viewer self.orientation = list(_lh_views_dict.keys()) self.default_smoothing_range = [0, 15] @@ -526,14 +524,13 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): # Direct access parameters: self._iren = self._renderer.plotter.iren + self.tool_bar = None if self.notebook: self.main_menu = None - self.tool_bar = None self.status_bar = None self.interactor = None else: 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 @@ -563,16 +560,16 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_time_label() self._configure_sliders() self._configure_scalar_bar() - self._configure_point_picking() self._configure_shortcuts() - if not self.notebook: - self._load_icons() + self._configure_point_picking() + self._configure_tool_bar() + if self.notebook: + self.show() + else: self._configure_playback() self._configure_menu() - self._configure_tool_bar() self._configure_status_bar() - if not self.notebook: # show everything at the end self.toggle_interface() with self.ensure_minimum_sizes(): @@ -1097,56 +1094,78 @@ def _save_movie_noname(self): return self.save_movie(None) def _configure_tool_bar(self): - self.actions["screenshot"] = self.tool_bar.addAction( - self.icons["screenshot"], - "Take a screenshot", - self.plotter._qt_screenshot - ) - self.actions["movie"] = self.tool_bar.addAction( - self.icons["movie"], - "Save movie...", - self._save_movie_noname, - ) - self.actions["visibility"] = self.tool_bar.addAction( - self.icons["visibility_on"], - "Toggle Visibility", - self.toggle_interface - ) - self.actions["play"] = self.tool_bar.addAction( - self.icons["play"], - "Play/Pause", - self.toggle_playback - ) - self.actions["reset"] = self.tool_bar.addAction( - self.icons["reset"], - "Reset", - self.reset - ) - self.actions["scale"] = self.tool_bar.addAction( - self.icons["scale"], - "Auto-Scale", - self.apply_auto_scaling - ) - self.actions["restore"] = self.tool_bar.addAction( - self.icons["restore"], - "Restore scaling", - self.restore_user_scaling - ) - self.actions["clear"] = self.tool_bar.addAction( - self.icons["clear"], - "Clear traces", - self.clear_points - ) - self.actions["help"] = self.tool_bar.addAction( - self.icons["help"], - "Help", - self.help - ) + if self.notebook: + from IPython import display + from ipywidgets import HBox, Button + self.actions["visibility"] = Button( + description="Toggle Visibility", + ) + self.actions["visibility"].on_click( + lambda x: self.toggle_interface()) + self.actions["reset"] = Button(description="Reset") + self.actions["reset"].on_click(lambda x: self.reset()) + self.actions["scale"] = Button(description="Auto-Scale") + self.actions["scale"].on_click(lambda x: self.apply_auto_scaling()) + self.actions["restore"] = Button(description="Restore scaling") + self.actions["restore"].on_click( + lambda x: self.restore_user_scaling()) + self.actions["clear"] = Button(description="Clear traces") + self.actions["clear"].on_click(lambda x: self.clear_points()) + self.tool_bar = HBox(tuple(self.actions.values())) + display.display(self.tool_bar) + else: + self._load_icons() + self.tool_bar = self.window.addToolBar("toolbar") + self.actions["screenshot"] = self.tool_bar.addAction( + self.icons["screenshot"], + "Take a screenshot", + self.plotter._qt_screenshot + ) + self.actions["movie"] = self.tool_bar.addAction( + self.icons["movie"], + "Save movie...", + self._save_movie_noname, + ) + self.actions["visibility"] = self.tool_bar.addAction( + self.icons["visibility_on"], + "Toggle Visibility", + self.toggle_interface + ) + self.actions["play"] = self.tool_bar.addAction( + self.icons["play"], + "Play/Pause", + self.toggle_playback + ) + self.actions["reset"] = self.tool_bar.addAction( + self.icons["reset"], + "Reset", + self.reset + ) + self.actions["scale"] = self.tool_bar.addAction( + self.icons["scale"], + "Auto-Scale", + self.apply_auto_scaling + ) + self.actions["restore"] = self.tool_bar.addAction( + self.icons["restore"], + "Restore scaling", + self.restore_user_scaling + ) + self.actions["clear"] = self.tool_bar.addAction( + self.icons["clear"], + "Clear traces", + self.clear_points + ) + self.actions["help"] = self.tool_bar.addAction( + self.icons["help"], + "Help", + self.help + ) - # Qt shortcuts - self.actions["movie"].setShortcut("ctrl+shift+s") - self.actions["play"].setShortcut(" ") - self.actions["help"].setShortcut("?") + # 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) @@ -1392,6 +1411,7 @@ def clear_points(self): assert sum(len(v) for v in self.picked_points.values()) == 0 assert len(self.pick_table) == 0 assert len(self._spheres) == 0 + self._update() def plot_time_course(self, hemi, vertex_id, color): """Plot the vertex time course. From 9a3d0fc0f4a7c920dcf6c5039ff19dd2d0387122 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 17:08:21 +0100 Subject: [PATCH 17/28] Reorder figures --- mne/viz/_brain/_brain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 0389565ffb2..ddb28ca3d59 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -592,10 +592,10 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): self._configure_shortcuts() self._configure_picking() self._configure_tool_bar() - self._configure_trace_mode() if self.notebook: self.show() - else: + self._configure_trace_mode() + if not self.notebook: self._configure_playback() self._configure_menu() self._configure_status_bar() From 137b7b140874e3148cec35ec8acc38725d92bb24 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Thu, 17 Dec 2020 17:12:05 +0100 Subject: [PATCH 18/28] Fix visibility --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index ddb28ca3d59..b78cc1bec09 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -595,13 +595,13 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True): if self.notebook: self.show() self._configure_trace_mode() + self.toggle_interface() if not self.notebook: self._configure_playback() self._configure_menu() self._configure_status_bar() # show everything at the end - self.toggle_interface() with self.ensure_minimum_sizes(): self.show() From 3c3160595d20dc0e2046e755ea4c8b66f0336cff Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 18 Dec 2020 10:28:21 +0100 Subject: [PATCH 19/28] Improve coverage --- mne/viz/_brain/tests/test.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb index fb2e7cfd317..80a8bec809e 100644 --- a/mne/viz/_brain/tests/test.ipynb +++ b/mne/viz/_brain/tests/test.ipynb @@ -48,6 +48,7 @@ " assert isinstance(brain, brain_class)\n", " assert brain.notebook\n", " assert brain._renderer.figure.display is not None\n", + " brain._update()\n", " brain.close()" ] }, From a23e3c60ce9db35816818e24a50016662241fcb0 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Fri, 18 Dec 2020 15:23:12 +0100 Subject: [PATCH 20/28] Fix ratio and layout --- mne/viz/_brain/_brain.py | 5 +++-- mne/viz/_brain/mplcanvas.py | 5 ++--- mne/viz/backends/_notebook.py | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index b78cc1bec09..52f4e70c0d5 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1002,12 +1002,13 @@ def _configure_playback(self): self.plotter.add_callback(self._play, self.refresh_rate_ms) def _configure_mplcanvas(self): + ratio = (1 - self.interactor_fraction) / self.interactor_fraction if self.notebook: - dpi = 100 + dpi = 96 w, h = self.plotter.window_size + h /= ratio else: dpi = self.window.windowHandle().screen().logicalDotsPerInch() - ratio = (1 - self.interactor_fraction) / self.interactor_fraction w = self.interactor.geometry().width() h = self.interactor.geometry().height() / ratio # Get the fractional components for the brain and mpl diff --git a/mne/viz/_brain/mplcanvas.py b/mne/viz/_brain/mplcanvas.py index 798d476d7c7..463005b4eaf 100644 --- a/mne/viz/_brain/mplcanvas.py +++ b/mne/viz/_brain/mplcanvas.py @@ -24,8 +24,7 @@ def __init__(self, brain, width, height, dpi, notebook=False): extra_events = ('resize',) self.notebook = notebook if self.notebook: - self.fig, self.axes = plt.subplots(figsize=(width, height), - dpi=dpi) + self.fig = plt.figure(figsize=(width, height), dpi=dpi) self.canvas = plt.gcf().canvas else: from PyQt5 import QtWidgets @@ -38,7 +37,6 @@ def __init__(self, brain, width, height, dpi, notebook=False): with context: self.fig = Figure(figsize=(width, height), dpi=dpi) self.canvas = FigureCanvasQTAgg(self.fig) - self.axes = self.fig.add_subplot(111) self.canvas.setParent(parent) FigureCanvasQTAgg.setSizePolicy( self.canvas, @@ -46,6 +44,7 @@ def __init__(self, brain, width, height, dpi, notebook=False): QtWidgets.QSizePolicy.Expanding ) FigureCanvasQTAgg.updateGeometry(self.canvas) + self.axes = self.fig.add_subplot(111) self.axes.set(xlabel='Time (sec)', ylabel='Activation (AU)') self.brain = brain self.time_func = brain.callbacks["time"] diff --git a/mne/viz/backends/_notebook.py b/mne/viz/backends/_notebook.py index 43aa11292e8..761f0b8a60f 100644 --- a/mne/viz/backends/_notebook.py +++ b/mne/viz/backends/_notebook.py @@ -17,6 +17,7 @@ def show(self): from IPython.display import display self.figure.display = self.plotter.show(use_ipyvtk=True, return_viewer=True) + self.figure.display.layout.width = None # unlock the fixed layout display(self.figure.display) return self.scene() From b16738ce3cf34fcbe569412c92a71de497a67616 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Mon, 4 Jan 2021 13:53:18 +0100 Subject: [PATCH 21/28] Update changes --- doc/changes/latest.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 21c84feafbc..87ed238e996 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -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 ~~~~ From 195c91beada07fa81fc3f24bb8141f72162e9418 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 5 Jan 2021 14:32:24 +0100 Subject: [PATCH 22/28] Do not import pyplot --- mne/viz/_brain/mplcanvas.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/mne/viz/_brain/mplcanvas.py b/mne/viz/_brain/mplcanvas.py index 463005b4eaf..6870f6ada12 100644 --- a/mne/viz/_brain/mplcanvas.py +++ b/mne/viz/_brain/mplcanvas.py @@ -12,9 +12,9 @@ class MplCanvas(object): """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" def __init__(self, brain, width, height, dpi, notebook=False): - import matplotlib.pyplot as plt from matplotlib import rc_context from matplotlib.figure import Figure + self.notebook = notebook if brain.separate_canvas: parent = None else: @@ -22,20 +22,21 @@ def __init__(self, brain, width, height, dpi, notebook=False): # prefer constrained layout here but live with tight_layout otherwise context = nullcontext extra_events = ('resize',) - self.notebook = notebook + try: + context = rc_context({'figure.constrained_layout.use': True}) + extra_events = () + except KeyError: + pass + with context: + self.fig = Figure(figsize=(width, height), dpi=dpi) if self.notebook: - self.fig = plt.figure(figsize=(width, height), dpi=dpi) - self.canvas = plt.gcf().canvas + from matplotlib.backends.backend_nbagg import (FigureCanvasNbAgg, + FigureManager) + self.canvas = FigureCanvasNbAgg(self.fig) + self.manager = FigureManager(self.canvas, 0) else: from PyQt5 import QtWidgets from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg - try: - context = rc_context({'figure.constrained_layout.use': True}) - extra_events = () - except KeyError: - pass - with context: - self.fig = Figure(figsize=(width, height), dpi=dpi) self.canvas = FigureCanvasQTAgg(self.fig) self.canvas.setParent(parent) FigureCanvasQTAgg.setSizePolicy( @@ -44,6 +45,7 @@ def __init__(self, brain, width, height, dpi, notebook=False): QtWidgets.QSizePolicy.Expanding ) FigureCanvasQTAgg.updateGeometry(self.canvas) + self.manager = None self.axes = self.fig.add_subplot(111) self.axes.set(xlabel='Time (sec)', ylabel='Activation (AU)') self.brain = brain @@ -92,7 +94,9 @@ def set_color(self, bg_color, fg_color): def show(self): """Show the canvas.""" - if hasattr(self.canvas, "show"): + if self.notebook: + self.manager.show() + else: self.canvas.show() def close(self): @@ -115,6 +119,7 @@ def clear(self): self.fig.clear() self.brain = None self.canvas = None + self.manager = None on_motion_notify = on_button_press # for now they can be the same From ed680d3b7e52fcec70b1aedad7873d3fa8da5d20 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Tue, 5 Jan 2021 14:55:41 +0100 Subject: [PATCH 23/28] Update mne/viz/_brain/_brain.py Co-authored-by: Eric Larson --- mne/viz/_brain/_brain.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 52f4e70c0d5..67642b2c1d9 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1006,11 +1006,11 @@ def _configure_mplcanvas(self): if self.notebook: dpi = 96 w, h = self.plotter.window_size - h /= ratio else: dpi = self.window.windowHandle().screen().logicalDotsPerInch() w = self.interactor.geometry().width() - h = self.interactor.geometry().height() / ratio + 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.notebook) From 57b505fe338eecb437638c8a5534a0aa468d8eb2 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 6 Jan 2021 14:45:39 +0100 Subject: [PATCH 24/28] Use _add_action --- mne/viz/_brain/_brain.py | 89 +++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 67642b2c1d9..501d6255698 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1214,29 +1214,59 @@ def _load_icons(self): def _save_movie_noname(self): return self.save_movie(None) + 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=None): + if self.notebook: + from ipywidgets import Button + self.actions[name] = Button(description=name) + self.actions[name].on_click(lambda x: func()) + else: + icon_name = name if icon_name is None else icon_name + self.actions[name] = self.tool_bar.addAction( + self.icons[icon_name], + desc, + self.plotter._qt_screenshot + ) + def _configure_tool_bar(self): + self._initialize_actions() + self._add_action( + name="visibility", + desc="Toggle Visibility", + func=self.toggle_interface, + icon_name="visibility_on" + ) + self._add_action( + name="reset", + desc="Reset", + func=self.reset, + ) + self._add_action( + name="scale", + desc="Auto-Scale", + func=self.apply_auto_scaling, + ) + self._add_action( + name="restore", + desc="Restore scaling", + func=self.restore_user_scaling, + ) + self._add_action( + name="clear", + desc="Clear traces", + func=self.clear_glyphs, + ) + if self.notebook: from IPython import display - from ipywidgets import HBox, Button - self.actions["visibility"] = Button( - description="Toggle Visibility", - ) - self.actions["visibility"].on_click( - lambda x: self.toggle_interface()) - self.actions["reset"] = Button(description="Reset") - self.actions["reset"].on_click(lambda x: self.reset()) - self.actions["scale"] = Button(description="Auto-Scale") - self.actions["scale"].on_click(lambda x: self.apply_auto_scaling()) - self.actions["restore"] = Button(description="Restore scaling") - self.actions["restore"].on_click( - lambda x: self.restore_user_scaling()) - self.actions["clear"] = Button(description="Clear traces") - self.actions["clear"].on_click(lambda x: self.clear_glyphs()) + from ipywidgets import HBox self.tool_bar = HBox(tuple(self.actions.values())) display.display(self.tool_bar) else: - self._load_icons() - self.tool_bar = self.window.addToolBar("toolbar") self.actions["screenshot"] = self.tool_bar.addAction( self.icons["screenshot"], "Take a screenshot", @@ -1247,36 +1277,11 @@ def _configure_tool_bar(self): "Save movie...", self._save_movie_noname, ) - self.actions["visibility"] = self.tool_bar.addAction( - self.icons["visibility_on"], - "Toggle Visibility", - self.toggle_interface - ) self.actions["play"] = self.tool_bar.addAction( self.icons["play"], "Play/Pause", self.toggle_playback ) - self.actions["reset"] = self.tool_bar.addAction( - self.icons["reset"], - "Reset", - self.reset - ) - self.actions["scale"] = self.tool_bar.addAction( - self.icons["scale"], - "Auto-Scale", - self.apply_auto_scaling - ) - self.actions["restore"] = self.tool_bar.addAction( - self.icons["restore"], - "Restore scaling", - self.restore_user_scaling - ) - self.actions["clear"] = self.tool_bar.addAction( - self.icons["clear"], - "Clear traces", - self.clear_glyphs - ) self.actions["help"] = self.tool_bar.addAction( self.icons["help"], "Help", From 40590827ed2d59de739c314097da112de7363289 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 6 Jan 2021 15:01:14 +0100 Subject: [PATCH 25/28] Use icon + tooltip --- mne/viz/_brain/_brain.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 501d6255698..04c21b1be2f 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1219,15 +1219,15 @@ def _initialize_actions(self): self._load_icons() self.tool_bar = self.window.addToolBar("toolbar") - def _add_action(self, name, desc, func, icon_name=None): + def _add_action(self, name, desc, func, icon_name, qt_icon_name=None): if self.notebook: from ipywidgets import Button - self.actions[name] = Button(description=name) + self.actions[name] = Button(tooltip=desc, icon=icon_name) self.actions[name].on_click(lambda x: func()) else: - icon_name = name if icon_name is None else icon_name + qt_icon_name = name if qt_icon_name is None else qt_icon_name self.actions[name] = self.tool_bar.addAction( - self.icons[icon_name], + self.icons[qt_icon_name], desc, self.plotter._qt_screenshot ) @@ -1238,27 +1238,32 @@ def _configure_tool_bar(self): name="visibility", desc="Toggle Visibility", func=self.toggle_interface, - icon_name="visibility_on" + icon_name="eye", + qt_icon_name="visibility_on", ) self._add_action( name="reset", desc="Reset", func=self.reset, + icon_name="history", ) self._add_action( name="scale", desc="Auto-Scale", func=self.apply_auto_scaling, + icon_name="magic", ) self._add_action( name="restore", desc="Restore scaling", func=self.restore_user_scaling, + icon_name="reply", ) self._add_action( name="clear", desc="Clear traces", func=self.clear_glyphs, + icon_name="trash", ) if self.notebook: From af7f9882f3ad14fc193b0239ad644d59241938ad Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 6 Jan 2021 15:01:29 +0100 Subject: [PATCH 26/28] Fix qt func --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 04c21b1be2f..c376cf795ff 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1229,7 +1229,7 @@ def _add_action(self, name, desc, func, icon_name, qt_icon_name=None): self.actions[name] = self.tool_bar.addAction( self.icons[qt_icon_name], desc, - self.plotter._qt_screenshot + func, ) def _configure_tool_bar(self): From a5e44e722ead565521a5e5cd8ecb8879277cb704 Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 6 Jan 2021 15:56:49 +0100 Subject: [PATCH 27/28] Reorder actions --- mne/viz/_brain/_brain.py | 58 +++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index c376cf795ff..eec63e9e0b5 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1214,13 +1214,20 @@ 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): + 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(tooltip=desc, icon=icon_name) self.actions[name].on_click(lambda x: func()) @@ -1234,6 +1241,20 @@ def _add_action(self, name, desc, func, icon_name, qt_icon_name=None): def _configure_tool_bar(self): self._initialize_actions() + self._add_action( + name="screenshot", + desc="Take a screenshot", + func=self._screenshot, + icon_name=None, + notebook=False, + ) + self._add_action( + name="movie", + desc="Save movie...", + func=self._save_movie_noname, + icon_name=None, + notebook=False, + ) self._add_action( name="visibility", desc="Toggle Visibility", @@ -1241,6 +1262,13 @@ def _configure_tool_bar(self): icon_name="eye", qt_icon_name="visibility_on", ) + self._add_action( + name="play", + desc="Play/Pause", + func=self.toggle_playback, + icon_name=None, + notebook=False, + ) self._add_action( name="reset", desc="Reset", @@ -1265,6 +1293,13 @@ def _configure_tool_bar(self): func=self.clear_glyphs, icon_name="trash", ) + self._add_action( + name="help", + desc="Help", + func=self.help, + icon_name=None, + notebook=False, + ) if self.notebook: from IPython import display @@ -1272,27 +1307,6 @@ def _configure_tool_bar(self): self.tool_bar = HBox(tuple(self.actions.values())) display.display(self.tool_bar) else: - self.actions["screenshot"] = self.tool_bar.addAction( - self.icons["screenshot"], - "Take a screenshot", - self.plotter._qt_screenshot - ) - self.actions["movie"] = self.tool_bar.addAction( - self.icons["movie"], - "Save movie...", - self._save_movie_noname, - ) - self.actions["play"] = self.tool_bar.addAction( - self.icons["play"], - "Play/Pause", - self.toggle_playback - ) - self.actions["help"] = self.tool_bar.addAction( - self.icons["help"], - "Help", - self.help - ) - # Qt shortcuts self.actions["movie"].setShortcut("ctrl+shift+s") self.actions["play"].setShortcut(" ") From 0dcc7f6fafc06034b0115b7b6b4c842a30b4080c Mon Sep 17 00:00:00 2001 From: Guillaume Favelier Date: Wed, 6 Jan 2021 16:28:40 +0100 Subject: [PATCH 28/28] Switch to icon + description --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index eec63e9e0b5..142c0320192 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1229,7 +1229,7 @@ def _add_action(self, name, desc, func, icon_name, qt_icon_name=None, if not notebook: return from ipywidgets import Button - self.actions[name] = Button(tooltip=desc, icon=icon_name) + 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