Skip to content

Commit 5bafe52

Browse files
marsipuagramfortlarsonerdrammock
authored
[ENH]: New pyqtgraph-backend for 2D-Data-Browser (#9687)
* start abstraction of Browser-Classes * outline of data-management-class * wip refactoring MNEBrowserFigure * refactored Browser-Initialization into MNEDataBrowser * fix some style-issues and remove remnants * start with integration of pyqtgraph-prototype * make pyqtgraph optional (just for development, can be removed again for final PR) * more refactoring of mpl-methods into BrowserBase * fixes for failing tests * add docstrings in _browser.py * move _update_projector-call into BrowserBase * add use/set/get for browser * rename 2D to Browser to avoid confusion * move inheritance of BrowserBaser from MNEFigure to MNEBrowseFigure * refactored _annotation_helper from test_raw.py * fix flake * move base-classes/funcs from _browser.py back to _figure.py to facilitate review * update from upstream * remove set/get/use-browser from __init__.py again to prevent make docstring from failing * add show_browser and other adjustments for pyqtgraph * refactoring _close into BrowserBase * add block for pyqtgraph * [revert for PR] adjust plot-function to accept kwargs from benchmark * fix annotation-key still working when plotting epochs * add docstring to show_browser * refactor midpoints into BrowserBase for epochs * add show to show_broser * add pyqtgraph to browse_backend-fixture * fix block not supported in Figure.show() * WIP adapt test_raw.test_plot_raw_traces * adapt test_plot_raw_traces to make it work for matplotlib and pyqtgraph * refactor _redraw * refactor _update_data * change bad-color from rgb to hex * pyqtgraph always blocks execution * reinsert block for pyqtgraph * make _redraw not abstract anymore (not needed for pyqtgraph) * add pyqtgraph-backend * fix butterfly-bug showing channels still in old y-position [ci skip] * rebase on main [ci skip] * Set usage of OpenGL to false by default * organize keyboard-shortcuts [ci skip] * clarify index-system for traces and set z-values for traces and annotations [ci skip] * add exception-hook from pytest-qt [ci skip] * actually raise exceptions from qt [ci skip] * fix bugs annotations (removing decription/select visible) [ci skip] * reorganize imports [ci skip] * fix bug _update_regions_colors [ci skip] * add '=' to keyboard-shortcuts and make scale-steps smaller [ci skip] * import pg-backend from separate repo[ci skip] * remove pg-backend from PR[ci skip] * update repo-link to mne-tools[ci skip] * Update mne/viz/_figure.py Co-authored-by: Alexandre Gramfort <[email protected]> * remove _pg_figure from _backends[ci skip] * add browser-backend-functions to documentation[ci skip] * avoid codespell-failure [ci skip] * remove pyqtgraph from tests (tests will be run in mne-qt-browser for now) * update parameters for Raw.plot() * fix flake * fix pip install link * fix table in set_browser_backend * fix butterfly showing always all channels despite of selection * adjust test_scale_bar for pyqtgraph [ci skip] * refactor channel context figs [ci skip] * change to relative imports [ci skip] * refactor _new_child_figure [ci skip] * update some key-presses [ci skip] * adapt test_plot_raw_ssp_interaction to pyqtgraph [ci skip] * adapt test_plot_raw_child_figures to pyqtgraph [ci skip] * change default of event_lines to list [ci skip] * add drag to _fake_click [ci skip] * adapt test_annotations to pyqtgraph [ci skip] * adapt test_clock_xticks to pyqtgraph [ci skip] * remove install-question [ci skip] * adapt pyqtgraph-backend to test_plot_raw_selection [ci skip] * adapt pyqtgraph-backend to test_min_window_size [ci skip] * adapt pyqtgraph-backend to test_plot_raw_groupby [ci skip] * fix multiple tests [ci skip] * adapt annotation-test for pyqtgraph [ci skip] * fix more test-issues [ci skip] * add pyqtgraph to test_raw-suite [ci skip] * fix flake * update feature-grid * update from main branch * fix checkbox-click issue (inconsistent across OS) * fix test_plot_raw_ssp_interaction for linux * update from upstream2 * fix flake * simplify ssp_interaction * fix test_min_window_size for Windows-CI * fix flake * implement review-feedback #1 * fix _proj_click_all for inconsistent fake-click-behaviour * clarify docs for preload * fix flake * [Refactor]: browser_backend-fixture for consistency * DOC: Add doc comments for block * add speed test * rename preload to precompute * fix unused import * remove unnecessary parameters * remove speed-test * remove automatic installation of mne-qt-browser * revert removal of automatic installation until mne-qt-browser is uploaded to PyPi * FIX: Route through call * add mne-qt-browser to requirements.txt * update latest.inc * fix flake * fix latest.inc * update test-dependencies * specify docs regarding block-behaviour * fix typo in github_actions_dependencies.sh * fix docstring for plot_raw * fix docstring for plot_raw again * fix docstring for plot_raw * add mne-qt-browser to azure_dependencies.sh * add mne-qt-browser to environment.yml * specify latest.inc * specifiy use_opengl documentation * Update mne/viz/_figure.py Co-authored-by: Eric Larson <[email protected]> * FIX: Fix test [skip azp] [skip circle] * FIX: Path [skip azp] [skip circle] * revert color-name-changes * MAINT: Test only on one run * FIX: Remove * MNT: Add to mne sys_info * DOC: Fix doc build * FIX: Better * DOC: Add link to mne-qt-browser issues in docs * Update mne/utils/docs.py Co-authored-by: Daniel McCloy <[email protected]> * Update mne/utils/docs.py Co-authored-by: Daniel McCloy <[email protected]> * Update mne/viz/raw.py Co-authored-by: Daniel McCloy <[email protected]> * FIX: fix flake * FIX: Test * FIX: Correct check * STY: Flake * FIX: One more mark Co-authored-by: Alexandre Gramfort <[email protected]> Co-authored-by: Eric Larson <[email protected]> Co-authored-by: Daniel McCloy <[email protected]>
1 parent b97da43 commit 5bafe52

25 files changed

+876
-532
lines changed

.github/workflows/compat_minimal.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ jobs:
5555
run: ./tools/get_testing_version.sh
5656
name: 'Get testing version'
5757
- shell: bash -el {0}
58-
run: MNE_SKIP_TESTING_DATASET_TESTS=true pytest -m "not ultraslowtest" --tb=short --cov=mne --cov-report xml -vv -rfE mne/
58+
run: MNE_SKIP_TESTING_DATASET_TESTS=true pytest -m "not (ultraslowtest or pgtest)" --tb=short --cov=mne --cov-report xml -vv -rfE mne/
5959
name: Run tests with no testing data
6060
- uses: actions/cache@v2
6161
with:

azure-pipelines.yml

+7-5
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ stages:
8181
variables:
8282
AZURE_CI: 'true'
8383
jobs:
84-
- job: Ultraslow
84+
- job: Ultraslow+PG
8585
pool:
8686
vmImage: 'ubuntu-20.04'
8787
variables:
@@ -111,6 +111,8 @@ stages:
111111
- bash: |
112112
set -e
113113
python -m pip install --progress-bar off --upgrade pip setuptools wheel codecov
114+
python -m pip install --progress-bar off mne-qt-browser
115+
python -m pip uninstall -yq mne
114116
python -m pip install --progress-bar off --upgrade -e .[test]
115117
displayName: 'Install dependencies with pip'
116118
- script: mne sys_info -pd
@@ -124,8 +126,8 @@ stages:
124126
displayName: 'Cache testing data'
125127
- script: python -c "import mne; mne.datasets.testing.data_path(verbose=True)"
126128
displayName: 'Get test data'
127-
- script: pytest -m "ultraslowtest" --tb=short --cov=mne --cov-report=xml --cov-report=html -vv mne
128-
displayName: 'Run ultraslow tests'
129+
- script: pytest -m "ultraslowtest or pgtest" --tb=short --cov=mne --cov-report=xml --cov-report=html -vv mne
130+
displayName: 'Run ultraslow and PyQtGraph mne-qt-browser tests'
129131
- bash: bash <(curl -s https://codecov.io/bash)
130132
displayName: 'Codecov'
131133
condition: succeededOrFailed()
@@ -169,7 +171,7 @@ stages:
169171
displayName: 'Cache testing data'
170172
- script: python -c "import mne; mne.datasets.testing.data_path(verbose=True)"
171173
displayName: 'Get test data'
172-
- script: pytest --tb=short --cov=mne --cov-report=xml --cov-report=html -vv mne/viz
174+
- script: pytest --tb=short -m "not pgtest" --cov=mne --cov-report=xml --cov-report=html -vv mne/viz
173175
displayName: 'Run viz tests'
174176
- bash: bash <(curl -s https://codecov.io/bash)
175177
displayName: 'Codecov'
@@ -264,7 +266,7 @@ stages:
264266
displayName: 'Cache testing data'
265267
- script: python -c "import mne; mne.datasets.testing.data_path(verbose=True)"
266268
displayName: 'Get test data'
267-
- script: pytest -m "not slowtest" --tb=short --cov=mne --cov-report=xml --cov-report=html -vv mne
269+
- script: pytest -m "not (slowtest or pgtest)" --tb=short --cov=mne --cov-report=xml --cov-report=html -vv mne
268270
displayName: 'Run tests'
269271
- bash: bash <(curl -s https://codecov.io/bash)
270272
displayName: 'Codecov'

doc/changes/latest.inc

+5-1
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,14 @@ Enhancements
211211

212212
- Add support for colormap normalization in :meth:`mne.time_frequency.AverageTFR.plot` (:gh:`9851` by `Clemens Brunner`_)
213213

214-
- Add support for BIDS-compatible filenames when splitting big epochs files via the new ``split_naming`` parameter in :meth:`mne.Epochs.save` (:gh:9869 by `Denis Engemann`_)
214+
- Add support for BIDS-compatible filenames when splitting big epochs files via the new ``split_naming`` parameter in :meth:`mne.Epochs.save` (:gh:`9869` by `Denis Engemann`_)
215215

216216
- Add ``by_event_type`` parameter to :meth:`mne.Epochs.average` to create a list containing an :class:`mne.Evoked` object for each event type (:gh:`9859` by `Marijn van Vliet`_)
217217

218+
- Add pyqtgraph as a new backend for :meth:`mne.io.Raw.plot` (:gh:`9687` by `Martin Schulz`_)
219+
220+
- Add :func:`mne.viz.set_browser_backend`, :func:`mne.viz.use_browser_backend` and :func:`mne.viz.get_browser_backend` to set matplotlib or pyqtgraph as backend for :meth:`mne.io.Raw.plot` (:gh:`9687` by `Martin Schulz`_)
221+
218222
Bugs
219223
~~~~
220224
- Fix bug in :meth:`mne.io.Raw.pick` and related functions when parameter list contains channels which are not in info instance (:gh:`9708` **by new contributor** |Evgeny Goldstein|_)

doc/cited.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Papers citing MNE-Python
44
========================
55

6-
Estimates provided by Google Scholar as of 27 January 2021:
6+
Estimates provided by Google Scholar as of 02 November 2021:
77

8-
- `MNE (908) <https://scholar.google.com/scholar?cites=12188330066413208874&as_ylo=2014>`_
9-
- `MNE-Python (771) <https://scholar.google.com/scholar?cites=1521584321377182930&as_ylo=2013>`_
8+
- `MNE (1100) <https://scholar.google.com/scholar?cites=12188330066413208874&as_ylo=2014>`_
9+
- `MNE-Python (1060) <https://scholar.google.com/scholar?cites=1521584321377182930&as_ylo=2013>`_

doc/visualization.rst

+3
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,6 @@ Visualization
7979
close_3d_figure
8080
close_all_3d_figures
8181
get_brain_class
82+
set_browser_backend
83+
get_browser_backend
84+
use_browser_backend

environment.yml

+1
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ dependencies:
4242
- pooch
4343
- pip:
4444
- ipyvtklink
45+
- mne-qt-browser

mne/commands/mne_browse_raw.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
def run():
2323
"""Run command."""
24-
import matplotlib.pyplot as plt
2524
from mne.commands.utils import get_optparser, _add_verbose_flag
2625
from mne.viz import _RAW_CLIP_DEF
2726

@@ -135,8 +134,8 @@ def run():
135134
raw.plot(duration=duration, start=start, n_channels=n_channels,
136135
group_by=group_by, show_options=show_options, events=events,
137136
highpass=highpass, lowpass=lowpass, filtorder=filtorder,
138-
clipping=clipping, proj=not proj_off, verbose=verbose)
139-
plt.show(block=True)
137+
clipping=clipping, proj=not proj_off, verbose=verbose,
138+
show=True, block=True)
140139

141140

142141
mne.utils.run_command_if_main()

mne/conftest.py

+48-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
from mne.fixes import has_numba
2626
from mne.io import read_raw_fif, read_raw_ctf
2727
from mne.stats import cluster_level
28-
from mne.utils import _pl, _assert_no_instances, numerics, Bunch
28+
from mne.utils import (_pl, _assert_no_instances, numerics, Bunch,
29+
_check_pyqt5_version)
2930

3031
# data from sample dataset
3132
from mne.viz._figure import use_browser_backend
@@ -61,7 +62,7 @@
6162
def pytest_configure(config):
6263
"""Configure pytest options."""
6364
# Markers
64-
for marker in ('slowtest', 'ultraslowtest'):
65+
for marker in ('slowtest', 'ultraslowtest', 'pgtest'):
6566
config.addinivalue_line('markers', marker)
6667

6768
# Fixtures
@@ -397,11 +398,52 @@ def garbage_collect():
397398
gc.collect()
398399

399400

400-
@pytest.fixture(params=['matplotlib'])
401-
def browse_backend(request, garbage_collect):
401+
@pytest.fixture
402+
def mpl_backend(garbage_collect):
403+
"""Use for epochs/ica when not implemented with pyqtgraph yet."""
404+
with use_browser_backend('matplotlib') as backend:
405+
yield backend
406+
backend._close_all()
407+
408+
409+
def _check_pyqtgraph():
410+
try:
411+
import PyQt5 # noqa: F401
412+
except ModuleNotFoundError:
413+
pytest.skip('PyQt5 is not installed but needed for pyqtgraph!')
414+
try:
415+
assert LooseVersion(_check_pyqt5_version()) >= LooseVersion('5.12')
416+
except AssertionError:
417+
pytest.skip(f'PyQt5 has version {_check_pyqt5_version()}'
418+
f'but pyqtgraph needs >= 5.12!')
419+
try:
420+
import mne_qt_browser # noqa: F401
421+
except Exception:
422+
pytest.skip('Requires mne_qt_browser')
423+
424+
425+
@pytest.mark.pgtest
426+
@pytest.fixture
427+
def pg_backend(garbage_collect):
428+
"""Use for pyqtgraph-specific test-functions."""
429+
_check_pyqtgraph()
430+
with use_browser_backend('pyqtgraph') as backend:
431+
yield backend
432+
backend._close_all()
433+
434+
435+
@pytest.fixture(params=[
436+
'matplotlib',
437+
pytest.param('pyqtgraph', marks=pytest.mark.pgtest),
438+
])
439+
def browser_backend(request, garbage_collect):
402440
"""Parametrizes the name of the browser backend."""
403-
with use_browser_backend(request.param) as backend:
441+
backend_name = request.param
442+
if backend_name == 'pyqtgraph':
443+
_check_pyqtgraph()
444+
with use_browser_backend(backend_name) as backend:
404445
yield backend
446+
backend._close_all()
405447

406448

407449
@pytest.fixture(params=["mayavi", "pyvistaqt"])
@@ -446,7 +488,7 @@ def renderer_interactive(request):
446488
if renderer._get_3d_backend() == 'mayavi':
447489
with warnings.catch_warnings(record=True):
448490
try:
449-
from surfer import Brain # noqa: 401 analysis:ignore
491+
from surfer import Brain # noqa: F401 analysis:ignore
450492
except Exception:
451493
pytest.skip('Requires PySurfer')
452494
yield renderer

mne/io/base.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1517,22 +1517,23 @@ def _tmin_tmax_to_start_stop(self, tmin, tmax):
15171517

15181518
@copy_function_doc_to_method_doc(plot_raw)
15191519
def plot(self, events=None, duration=10.0, start=0.0, n_channels=20,
1520-
bgcolor='w', color=None, bad_color=(0.8, 0.8, 0.8),
1520+
bgcolor='w', color=None, bad_color='lightgray',
15211521
event_color='cyan', scalings=None, remove_dc=True, order=None,
15221522
show_options=False, title=None, show=True, block=False,
15231523
highpass=None, lowpass=None, filtorder=4, clipping=_RAW_CLIP_DEF,
15241524
show_first_samp=False, proj=True, group_by='type',
15251525
butterfly=False, decim='auto', noise_cov=None, event_id=None,
15261526
show_scrollbars=True, show_scalebars=True, time_format='float',
1527-
verbose=None):
1527+
precompute='auto', use_opengl=True, verbose=None):
15281528
return plot_raw(self, events, duration, start, n_channels, bgcolor,
15291529
color, bad_color, event_color, scalings, remove_dc,
15301530
order, show_options, title, show, block, highpass,
15311531
lowpass, filtorder, clipping, show_first_samp,
15321532
proj, group_by, butterfly, decim, noise_cov=noise_cov,
15331533
event_id=event_id, show_scrollbars=show_scrollbars,
1534-
show_scalebars=show_scalebars,
1535-
time_format=time_format, verbose=verbose)
1534+
show_scalebars=show_scalebars, time_format=time_format,
1535+
precompute=precompute, use_opengl=use_opengl,
1536+
verbose=verbose)
15361537

15371538
@verbose
15381539
@copy_function_doc_to_method_doc(plot_raw_psd)

mne/utils/config.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user'):
512512
""" # noqa: E501
513513
_validate_type(dependencies, str)
514514
_check_option('dependencies', dependencies, ('user', 'developer'))
515-
ljust = 21 if dependencies == 'developer' else 15
515+
ljust = 21 if dependencies == 'developer' else 16
516516
platform_str = platform.platform()
517517
if platform.system() == 'Darwin' and sys.version_info[:2] < (3, 8):
518518
# platform.platform() in Python < 3.8 doesn't call
@@ -548,7 +548,7 @@ def sys_info(fid=None, show_paths=False, *, dependencies='user'):
548548
use_mod_names = ('mne', 'numpy', 'scipy', 'matplotlib', '', 'sklearn',
549549
'numba', 'nibabel', 'nilearn', 'dipy', 'cupy', 'pandas',
550550
'mayavi', 'pyvista', 'pyvistaqt', 'ipyvtklink', 'vtk',
551-
'PyQt5', 'ipympl')
551+
'PyQt5', 'ipympl', 'mne_qt_browser')
552552
if dependencies == 'developer':
553553
use_mod_names += (
554554
'', 'sphinx', 'sphinx_gallery', 'numpydoc', 'pydata_sphinx_theme',

mne/utils/docs.py

+23
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,29 @@
15151515
.. versionadded:: 0.24
15161516
"""
15171517

1518+
# Visualization with pyqtgraph
1519+
docdict['precompute'] = """
1520+
precompute : bool | str
1521+
Whether to load all data (not just the visible portion) into RAM and
1522+
apply preprocessing (e.g., projectors) to the full data array in a separate
1523+
processor thread, instead of window-by-window during scrolling. The default
1524+
``'auto'`` compares available RAM space to the expected size of the
1525+
precomputed data, and precomputes only if enough RAM is available. ``True``
1526+
and ``'auto'`` only work if using the pyQtGraph backend.
1527+
1528+
.. versionadded:: 0.24
1529+
"""
1530+
1531+
docdict['use_opengl'] = """
1532+
use_opengl : bool
1533+
Whether to use OpenGL when rendering the plot (requires ``pyopengl``).
1534+
May increase performance, but effect is dependent on system CPU and
1535+
graphics hardware. Only works if using the pyQtGraph backend. Default is
1536+
``True``.
1537+
1538+
.. versionadded:: 0.24
1539+
"""
1540+
15181541
# PSD plotting
15191542
docdict["plot_psd_doc"] = """
15201543
Plot the power spectral density across channels.

mne/utils/tests/test_config.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def test_sys_info():
8686
assert ('numpy:' in out)
8787

8888
if platform.system() == 'Darwin':
89-
assert 'Platform: macOS-' in out
89+
assert 'Platform: macOS-' in out
90+
elif platform.system() == 'Linux':
91+
assert 'Platform: Linux' in out
9092

9193

9294
def test_get_subjects_dir(tmp_path, monkeypatch):

mne/viz/__init__.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
from .montage import plot_montage
3131
from .backends.renderer import (set_3d_backend, get_3d_backend, use_3d_backend,
3232
set_3d_view, set_3d_title, create_3d_figure,
33-
close_3d_figure, close_all_3d_figures, get_brain_class)
33+
close_3d_figure, close_all_3d_figures,
34+
get_brain_class)
3435
from . import backends
3536
from ._brain import Brain
37+
from ._figure import (get_browser_backend, set_browser_backend,
38+
use_browser_backend)

0 commit comments

Comments
 (0)