File: test_raw.py

package info (click to toggle)
python-mne 1.3.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 100,172 kB
  • sloc: python: 166,349; pascal: 3,602; javascript: 1,472; sh: 334; makefile: 236
file content (975 lines) | stat: -rw-r--r-- 40,569 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
# Authors: Eric Larson <larson.eric.d@gmail.com>
#
# License: Simplified BSD

import itertools
import os
from copy import deepcopy

import matplotlib
import matplotlib.pyplot as plt
from matplotlib import backend_bases
import numpy as np
import pytest
from numpy.testing import assert_allclose

from mne import Annotations, create_info, pick_types
from mne.annotations import _sync_onset
from mne.datasets import testing
from mne.io import RawArray
from mne.io.pick import _DATA_CH_TYPES_ORDER_DEFAULT, _PICK_TYPES_DATA_DICT
from mne.utils import (_dt_to_stamp, _record_warnings, get_config, set_config,
                       _assert_no_instances)
from mne.viz import plot_raw, plot_sensors
from mne.viz.utils import _fake_click, _fake_keypress


def _annotation_helper(raw, browse_backend, events=False):
    """Test interactive annotations."""
    ismpl = browse_backend.name == 'matplotlib'
    # Some of our checks here require modern mpl to work properly
    n_anns = len(raw.annotations)
    browse_backend._close_all()

    if events:
        events = np.array([[raw.first_samp + 100, 0, 1],
                           [raw.first_samp + 300, 0, 3]])
        n_events = len(events)
    else:
        events = None
        n_events = 0
    fig = raw.plot(events=events)
    if ismpl:
        assert browse_backend._get_n_figs() == 1

    fig._fake_keypress('a')  # annotation mode
    ann_fig = fig.mne.fig_annotation
    if ismpl:
        assert browse_backend._get_n_figs() == 2
        # +3 from the scale bars
        n_scale = 3
        assert len(fig.mne.ax_main.texts) == n_anns + n_events + n_scale
    else:
        assert ann_fig.isVisible()

    # modify description to create label "BAD test"
    # semicolon is ignored
    if ismpl:
        for key in ['backspace'] + list(' test;') + ['enter']:
            fig._fake_keypress(key, fig=ann_fig)
        # change annotation label
        for ix in (-1, 0):
            xy = ann_fig.mne.radio_ax.buttons.circles[ix].center
            fig._fake_click(xy, fig=ann_fig, ax=ann_fig.mne.radio_ax,
                            xform='data')
    else:
        # The modal dialogs of the Qt-backend would block the test,
        # thus a new description will be added programmatically.
        ann_fig._add_description('BAD test')

    # draw annotation
    fig._fake_click((1., 1.), add_points=[(5., 1.)], xform='data', button=1,
                    kind='drag')
    if ismpl:
        assert len(fig.mne.ax_main.texts) == n_anns + 1 + n_events + n_scale
        # test hover event
        fig._fake_keypress('p')  # first turn on draggable mode
        assert fig.mne.draggable_annotations
        hover_kwargs = dict(xform='data', button=None, kind='motion')
        fig._fake_click((4.6, 1.), **hover_kwargs)  # well inside ann.
        fig._fake_click((4.9, 1.), **hover_kwargs)  # almost at edge
        assert fig.mne.annotation_hover_line is not None
        fig._fake_click((5.5, 1.), **hover_kwargs)  # well outside ann.
        assert fig.mne.annotation_hover_line is None
        # more tests of hover line
        fig._fake_click((4.6, 1.), **hover_kwargs)  # well inside ann.
        fig._fake_click((4.9, 1.), **hover_kwargs)  # almost at edge
        assert fig.mne.annotation_hover_line is not None
        fig._fake_keypress('p')  # turn off draggable mode, then move a bit
        fig._fake_click((4.95, 1.), **hover_kwargs)
        assert fig.mne.annotation_hover_line is None
        fig._fake_keypress('p')  # turn draggable mode back on
    assert len(raw.annotations.onset) == n_anns + 1
    assert len(raw.annotations.duration) == n_anns + 1
    assert len(raw.annotations.description) == n_anns + 1
    assert raw.annotations.description[n_anns] == 'BAD test'
    onset = raw.annotations.onset[n_anns]
    want_onset = _sync_onset(raw, 1., inverse=True)
    # pyqtgraph: during the transformation from pixel-coordinates
    # to scene-coordinates when the click is simulated on QGraphicsView
    # with QTest, there seems to happen a rounding of pixels to integers
    # internally. This deviatian also seems to change between runs
    # (maybe device-dependent?).
    atol = 1e-10 if ismpl else 2e-2
    assert_allclose(onset, want_onset, atol=atol)
    assert_allclose(raw.annotations.duration[n_anns], 4., atol=atol)
    # modify annotation from end (duration 4 → 1.5)
    fig._fake_click((4.9, 1.), xform='data', button=1,
                    kind='motion')  # ease up to it
    fig._fake_click((5., 1.), add_points=[(2.5, 1.)], xform='data',
                    button=1, kind='drag')
    assert raw.annotations.onset[n_anns] == onset
    # 4 → 1.5
    assert_allclose(raw.annotations.duration[n_anns], 1.5, atol=atol)
    # modify annotation from beginning (duration 1.5 → 2.0)
    fig._fake_click((1., 1.), add_points=[(0.5, 1.)], xform='data', button=1,
                    kind='drag')
    assert_allclose(raw.annotations.onset[n_anns], onset - 0.5, atol=atol)
    # 1.5 → 2.0
    assert_allclose(raw.annotations.duration[n_anns], 2.0, atol=atol)
    assert len(raw.annotations.onset) == n_anns + 1
    assert len(raw.annotations.duration) == n_anns + 1
    assert len(raw.annotations.description) == n_anns + 1
    assert raw.annotations.description[n_anns] == 'BAD test'
    if ismpl:
        assert len(fig.axes[0].texts) == n_anns + 1 + n_events + n_scale
        fig._fake_keypress('shift+right')
        assert len(fig.axes[0].texts) == n_scale
        fig._fake_keypress('shift+left')
        assert len(fig.axes[0].texts) == n_anns + 1 + n_events + n_scale

    # draw another annotation merging the two
    fig._fake_click((5.5, 1.), add_points=[(2., 1.)],
                    xform='data', button=1, kind='drag')
    # delete the annotation
    assert len(raw.annotations.onset) == n_anns + 1
    assert len(raw.annotations.duration) == n_anns + 1
    assert len(raw.annotations.description) == n_anns + 1
    assert_allclose(raw.annotations.onset[n_anns], onset - 0.5, atol=atol)
    assert_allclose(raw.annotations.duration[n_anns], 5.0, atol=atol)
    if ismpl:
        assert len(fig.axes[0].texts) == n_anns + 1 + n_events + n_scale
    # Delete
    fig._fake_click((1.5, 1.), xform='data', button=3, kind='press')
    # exit, re-enter, then exit a different way
    fig._fake_keypress('a')  # exit
    fig._fake_keypress('a')  # enter
    assert len(raw.annotations.onset) == n_anns
    if ismpl:
        fig._fake_keypress('escape', fig=fig.mne.fig_annotation)  # exit again
        assert len(fig.axes[0].texts) == n_anns + n_events + n_scale
        fig._fake_keypress('shift+right')
        assert len(fig.axes[0].texts) == n_scale
        fig._fake_keypress('shift+left')
        assert len(fig.axes[0].texts) == n_anns + n_events + n_scale


def _proj_status(ssp_fig, browse_backend):
    if browse_backend.name == 'matplotlib':
        ax = ssp_fig.mne.proj_checkboxes.ax
        return [line.get_visible() for line
                in ax.findobj(matplotlib.lines.Line2D)][::2]
    else:
        return [chkbx.isChecked() for chkbx in ssp_fig.checkboxes]


def _proj_label(ssp_fig, browse_backend):
    if browse_backend.name == 'matplotlib':
        return [lb.get_text() for lb in ssp_fig.mne.proj_checkboxes.labels]
    else:
        return [chkbx.text() for chkbx in ssp_fig.checkboxes]


def _proj_click(idx, fig, browse_backend):
    ssp_fig = fig.mne.fig_proj
    if browse_backend.name == 'matplotlib':
        pos = np.array(ssp_fig.mne.proj_checkboxes.
                       labels[idx].get_position()) + 0.01

        fig._fake_click(pos, fig=ssp_fig, ax=ssp_fig.mne.proj_checkboxes.ax,
                        xform='data')
    else:
        # _fake_click on QCheckBox is inconsistent across platforms
        # (also see comment in test_plot_raw_selection).
        ssp_fig._proj_changed(not fig.mne.projs_on[idx], idx)
        # Update Checkbox
        ssp_fig.checkboxes[idx].setChecked(bool(fig.mne.projs_on[idx]))


def _proj_click_all(fig, browse_backend):
    ssp_fig = fig.mne.fig_proj
    if browse_backend.name == 'matplotlib':
        fig._fake_click((0.5, 0.5), fig=ssp_fig, ax=ssp_fig.mne.proj_all.ax)
        fig._fake_click((0.5, 0.5), fig=ssp_fig, ax=ssp_fig.mne.proj_all.ax,
                        kind='release')
    else:
        # _fake_click on QPushButton is inconsistent across platforms.
        ssp_fig.toggle_all()


def _spawn_child_fig(fig, attr, browse_backend, key):
    # starting state
    n_figs = browse_backend._get_n_figs()
    n_children = len(fig.mne.child_figs)
    # spawn the child fig
    fig._fake_keypress(key)
    # make sure the figure was actually spawned
    assert len(fig.mne.child_figs) == n_children + 1
    assert browse_backend._get_n_figs() == n_figs + 1
    # make sure the parent fig knows the child fig's name
    child_fig = getattr(fig.mne, attr)
    assert child_fig is not None
    return child_fig


def _destroy_child_fig(fig, child_fig, attr, browse_backend, key, key_target):
    # starting state
    n_figs = browse_backend._get_n_figs()
    n_children = len(fig.mne.child_figs)
    # destroy child fig (_close_event is MPL agg backend workaround)
    fig._fake_keypress(key, fig=key_target)
    fig._close_event(child_fig)
    # make sure the figure was actually destroyed
    assert len(fig.mne.child_figs) == n_children - 1
    assert browse_backend._get_n_figs() == n_figs - 1
    assert getattr(fig.mne, attr) is None


def _child_fig_helper(fig, key, attr, browse_backend):
    # Spawn and close child figs of raw.plot()
    assert getattr(fig.mne, attr) is None
    # spawn, then close via main window toggle
    child_fig = _spawn_child_fig(fig, attr, browse_backend, key)
    _destroy_child_fig(fig, child_fig, attr, browse_backend, key,
                       key_target=fig)
    # spawn again, then close via child window's close key
    child_fig = _spawn_child_fig(fig, attr, browse_backend, key)
    _destroy_child_fig(fig, child_fig, attr, browse_backend,
                       key=child_fig.mne.close_key,
                       key_target=child_fig)


def test_scale_bar(browser_backend):
    """Test scale bar for raw."""
    ismpl = browser_backend.name == 'matplotlib'
    sfreq = 1000.
    t = np.arange(10000) / sfreq
    data = np.sin(2 * np.pi * 10. * t)
    # ± 1000 fT, 400 fT/cm, 20 µV
    data = data * np.array([[1000e-15, 400e-13, 20e-6]]).T
    info = create_info(3, sfreq, ('mag', 'grad', 'eeg'))
    raw = RawArray(data, info)
    fig = raw.plot()
    texts = fig._get_scale_bar_texts()
    assert len(texts) == 3  # ch_type scale-bars
    wants = ('800.0 fT/cm', '2000.0 fT', '40.0 µV')
    assert texts == wants
    if ismpl:
        # 1 green vline, 3 data, 3 scalebars
        assert len(fig.mne.ax_main.lines) == 7
    else:
        assert len(fig.mne.scalebars) == 3
    for data, bar in zip(fig.mne.traces, fig.mne.scalebars.values()):
        y = data.get_ydata()
        y_lims = [y.min(), y.max()]
        bar_lims = bar.get_ydata()
        assert_allclose(y_lims, bar_lims, atol=1e-4)


def test_plot_raw_selection(raw, browser_backend):
    """Test selection mode of plot_raw()."""
    ismpl = browser_backend.name == 'matplotlib'
    with raw.info._unlock():
        raw.info['lowpass'] = 10.  # allow heavy decim during plotting
    browser_backend._close_all()           # ensure all are closed
    assert browser_backend._get_n_figs() == 0
    fig = raw.plot(group_by='selection', proj=False)
    assert browser_backend._get_n_figs() == 2
    sel_fig = fig.mne.fig_selection
    assert sel_fig is not None
    # test changing selection with arrow keys
    sel_dict = fig.mne.ch_selections
    assert len(fig.mne.traces) == len(sel_dict['Left-temporal'])  # 6
    fig._fake_keypress('down', fig=sel_fig)
    assert len(fig.mne.traces) == len(sel_dict['Left-frontal'])  # 3
    fig._fake_keypress('down', fig=sel_fig)
    assert len(fig.mne.traces) == len(sel_dict['Misc'])  # 1
    fig._fake_keypress('down', fig=sel_fig)  # ignored; no custom sel defined
    assert len(fig.mne.traces) == len(sel_dict['Misc'])  # 1
    # switch to butterfly mode
    fig._fake_keypress('b', fig=sel_fig)

    # ToDo: For Qt-backend the framework around RawTraceItem makes
    #  it difficult to show the same channel multiple times which is why
    #  it is currently not implemented.
    #  This would be relevant if you wanted to plot several selections in
    #  butterfly-mode which have some channels in common.
    sel_picks = len(np.concatenate(list(sel_dict.values())))
    if ismpl:
        assert len(fig.mne.traces) == sel_picks
    else:
        assert len(fig.mne.traces) == sel_picks - 1
    assert fig.mne.butterfly
    # test clicking on radio buttons → should cancel butterfly mode
    if ismpl:
        xy = sel_fig.mne.radio_ax.buttons.circles[0].center
        fig._fake_click(xy, fig=sel_fig, ax=sel_fig.mne.radio_ax, xform='data')
    else:
        # For an unknown reason test-clicking on checkboxes is inconsistent
        # across platforms.
        # (QTest.mouseClick works isolated on all platforms but somehow
        # not in this context. _fake_click isn't working on linux)
        sel_fig._chkbx_changed(list(sel_fig.chkbxs.keys())[0])
    assert len(fig.mne.traces) == len(sel_dict['Left-temporal'])  # 6
    assert not fig.mne.butterfly
    # test clicking on "custom" when not defined: should be no-op
    if ismpl:
        before_state = sel_fig.mne.radio_ax.buttons.value_selected
        xy = sel_fig.mne.radio_ax.buttons.circles[-1].center
        fig._fake_click(xy, fig=sel_fig, ax=sel_fig.mne.radio_ax, xform='data')
        lasso = sel_fig.lasso
        sensor_ax = sel_fig.mne.sensor_ax
        assert sel_fig.mne.radio_ax.buttons.value_selected == before_state
    else:
        before_state = sel_fig.mne.old_selection
        chkbx = sel_fig.chkbxs[list(sel_fig.chkbxs.keys())[-1]]
        fig._fake_click((0.5, 0.5), fig=chkbx)
        lasso = sel_fig.channel_fig.lasso
        sensor_ax = sel_fig.channel_widget
        assert before_state == sel_fig.mne.old_selection          # unchanged
    assert len(fig.mne.traces) == len(sel_dict['Left-temporal'])  # unchanged
    # test marking bad channel in selection mode → should make sensor red
    assert lasso.ec[:, 0].sum() == 0   # R of RGBA zero for all chans
    fig._click_ch_name(ch_index=1, button=1)  # mark bad
    assert lasso.ec[:, 0].sum() == 1   # one channel red
    fig._click_ch_name(ch_index=1, button=1)  # mark good
    assert lasso.ec[:, 0].sum() == 0   # all channels black
    # test lasso
    # Testing lasso-interactivity of sensor-plot within Qt-backend
    # with QTest doesn't seem to work.
    want = ['MEG 0111', 'MEG 0112', 'MEG 0113', 'MEG 0131', 'MEG 0132',
            'MEG 0133']
    assert want == sorted(fig.mne.ch_names[fig.mne.picks])
    want = ['MEG 0121', 'MEG 0122', 'MEG 0123']
    if ismpl:
        sel_fig._set_custom_selection()  # lasso empty → should do nothing
        # Lasso with 1 mag/grad sensor unit (upper left)
        fig._fake_click((0, 1), add_points=[(0.65, 1), (0.65, 0.7), (0, 0.7)],
                        fig=sel_fig, ax=sensor_ax, xform='ax', kind='drag')
    else:
        lasso.selection = want
        sel_fig._set_custom_selection()
    assert sorted(want) == sorted(fig.mne.ch_names[fig.mne.picks])
    # test joint closing of selection & data windows
    fig._fake_keypress(sel_fig.mne.close_key, fig=sel_fig)
    fig._close_event(sel_fig)
    assert browser_backend._get_n_figs() == 0


def test_plot_raw_ssp_interaction(raw, browser_backend):
    """Test SSP projector UI of plot_raw()."""
    with raw.info._unlock():
        raw.info['lowpass'] = 10.  # allow heavy decim during plotting
    # apply some (not all) projs to test our proj UI (greyed out applied projs)
    projs = raw.info['projs'][-2:]
    raw.del_proj([-2, -1])
    raw.apply_proj()
    raw.add_proj(projs)
    fig = raw.plot()
    # open SSP window
    fig._fake_keypress('j')
    assert browser_backend._get_n_figs() == 2
    ssp_fig = fig.mne.fig_proj
    assert _proj_status(ssp_fig, browser_backend) == [True, True, True]
    # this should have no effect (proj 0 is already applied)
    assert _proj_label(ssp_fig,
                       browser_backend)[0].endswith('(already applied)')
    _proj_click(0, fig, browser_backend)
    assert _proj_status(ssp_fig, browser_backend) == [True, True, True]
    # this should work (proj 1 not applied)
    _proj_click(1, fig, browser_backend)
    assert _proj_status(ssp_fig, browser_backend) == [True, False, True]
    # turn it back on
    _proj_click(1, fig, browser_backend)
    assert _proj_status(ssp_fig, browser_backend) == [True, True, True]
    # toggle all off (button axes need both press and release)
    _proj_click_all(fig, browser_backend)
    assert _proj_status(ssp_fig, browser_backend) == [True, False, False]
    fig._fake_keypress('J')
    assert _proj_status(ssp_fig, browser_backend) == [True, True, True]
    fig._fake_keypress('J')
    assert _proj_status(ssp_fig, browser_backend) == [True, False, False]
    # turn all on
    _proj_click_all(fig, browser_backend)
    assert fig.mne.projector is not None  # on
    assert _proj_status(ssp_fig, browser_backend) == [True, True, True]


def test_plot_raw_child_figures(raw, browser_backend):
    """Test spawning and closing of child figures."""
    ismpl = browser_backend.name == 'matplotlib'
    with raw.info._unlock():
        raw.info['lowpass'] = 10.  # allow heavy decim during plotting
    # make sure we start clean
    assert browser_backend._get_n_figs() == 0
    fig = raw.plot()
    assert browser_backend._get_n_figs() == 1
    # test child fig toggles
    _child_fig_helper(fig, '?', 'fig_help', browser_backend)
    _child_fig_helper(fig, 'j', 'fig_proj', browser_backend)
    if ismpl:  # in mne-qt-browser, annotation is a dock-widget, not a window
        _child_fig_helper(fig, 'a', 'fig_annotation', browser_backend)
    # test right-click → channel location popup
    fig._redraw()
    fig._click_ch_name(ch_index=2, button=3)
    assert len(fig.mne.child_figs) == 1
    assert browser_backend._get_n_figs() == 2
    fig._fake_keypress('escape', fig=fig.mne.child_figs[0])
    if ismpl:
        fig._close_event(fig.mne.child_figs[0])
    assert len(fig.mne.child_figs) == 0
    assert browser_backend._get_n_figs() == 1
    # test right-click on non-data channel
    ix = raw.get_channel_types().index('ias')  # find the shielding channel
    trace_ix = fig.mne.ch_order.tolist().index(ix)  # get its plotting position
    fig._redraw()
    fig._click_ch_name(ch_index=trace_ix, button=3)  # should be no-op
    assert len(fig.mne.child_figs) == 0
    assert browser_backend._get_n_figs() == 1
    # test resize of main window
    fig._resize_by_factor(0.5)


def test_orphaned_annot_fig(raw, browser_backend):
    """Test that annotation window is not orphaned (GH #10454)."""
    if browser_backend.name != 'matplotlib':
        return
    assert browser_backend._get_n_figs() == 0
    fig = raw.plot()
    _spawn_child_fig(fig, 'fig_annotation', browser_backend, 'a')
    fig._fake_keypress(key=fig.mne.close_key)
    fig._close_event()
    assert len(fig.mne.child_figs) == 0
    assert browser_backend._get_n_figs() == 0


def _monkeypatch_fig(fig, browser_backend):
    if browser_backend.name == 'matplotlib':
        fig.canvas.manager.full_screen_toggle = lambda: None
    else:
        # Monkeypatch the Qt methods
        def _full():
            fig.isFullScreen = lambda: True

        def _norm():
            fig.isFullScreen = lambda: False

        fig.showFullScreen = _full
        fig.showNormal = _norm


def test_plot_raw_keypresses(raw, browser_backend, monkeypatch):
    """Test keypress interactivity of plot_raw()."""
    with raw.info._unlock():
        raw.info['lowpass'] = 10.  # allow heavy decim during plotting
    fig = raw.plot()
    # test twice → once in normal, once in butterfly view.
    # NB: keys a, j, and ? are tested in test_plot_raw_child_figures()
    keys = ('pagedown', 'down', 'up', 'down', 'right', 'left', '-', '+', '=',
            'd', 'd', 'pageup', 'home', 'end', 'z', 'z', 's', 's', 'f11', 't',
            'b')
    # Avoid annoying fullscreen issues by monkey-patching our handlers
    _monkeypatch_fig(fig, browser_backend)
    # test for group_by='original'
    for key in 2 * keys + ('escape',):
        fig._fake_keypress(key)
    # test for group_by='selection'
    fig = plot_raw(raw, group_by='selection')
    _monkeypatch_fig(fig, browser_backend)
    for key in 2 * keys + ('escape',):
        fig._fake_keypress(key)


def test_plot_raw_traces(raw, events, browser_backend):
    """Test plotting of raw data."""
    ismpl = browser_backend.name == 'matplotlib'
    with raw.info._unlock():
        raw.info['lowpass'] = 10.  # allow heavy decim during plotting
    fig = raw.plot(events=events, order=[1, 7, 5, 2, 3], n_channels=3,
                   group_by='original')
    assert hasattr(fig, 'mne')  # make sure fig.mne param object is present
    if ismpl:
        assert len(fig.axes) == 5

    # setup
    x = fig.mne.traces[0].get_xdata()[5]
    y = fig.mne.traces[0].get_ydata()[5]
    hscroll = fig.mne.ax_hscroll
    vscroll = fig.mne.ax_vscroll
    # test marking bad channels
    label = fig._get_ticklabels('y')[0]
    assert label not in fig.mne.info['bads']
    # click data to mark bad
    fig._fake_click((x, y), xform='data')
    assert label in fig.mne.info['bads']
    # click data to unmark bad
    fig._fake_click((x, y), xform='data')
    assert label not in fig.mne.info['bads']
    # click name to mark bad
    fig._click_ch_name(ch_index=0, button=1)
    assert label in fig.mne.info['bads']
    # test other kinds of clicks
    fig._fake_click((0.5, 0.98))  # click elsewhere (add vline)
    assert fig.mne.vline_visible is True
    fig._fake_click((0.5, 0.98), button=3)  # remove vline
    assert fig.mne.vline_visible is False
    fig._fake_click((0.5, 0.5), ax=hscroll)  # change time
    t_start = fig.mne.t_start
    fig._fake_click((0.5, 0.5), ax=hscroll)  # shouldn't change time this time
    assert round(t_start, 6) == round(fig.mne.t_start, 6)
    # test scrolling through channels
    labels = fig._get_ticklabels('y')
    assert labels == [raw.ch_names[1], raw.ch_names[7], raw.ch_names[5]]
    fig._fake_click((0.5, 0.05), ax=vscroll)  # change channels to end
    labels = fig._get_ticklabels('y')
    assert labels == [raw.ch_names[5], raw.ch_names[2], raw.ch_names[3]]
    for _ in (0, 0):
        # first click changes channels to mid; second time shouldn't change
        # This needs to be changed for Qt, because there scrollbars are
        # drawn differently (value of slider at lower end, not at middle)
        yclick = 0.5 if ismpl else 0.7
        fig._fake_click((0.5, yclick), ax=vscroll)
        labels = fig._get_ticklabels('y')
        assert labels == [raw.ch_names[7], raw.ch_names[5], raw.ch_names[2]]

    # test clicking a channel name in butterfly mode
    bads = fig.mne.info['bads'].copy()
    fig._fake_keypress('b')
    fig._click_ch_name(ch_index=0, button=1)  # should be no-op
    assert fig.mne.info['bads'] == bads        # unchanged
    fig._fake_keypress('b')

    # test starting up in zen mode
    fig = plot_raw(raw, show_scrollbars=False)
    # test order, title, & show_options kwargs
    with pytest.raises(ValueError, match='order should be array-like; got'):
        raw.plot(order='foo')
    with pytest.raises(TypeError, match='title must be None or a string, got'):
        raw.plot(title=1)
    raw.plot(show_options=True)
    browser_backend._close_all()

    # annotations outside data range
    annot = Annotations([10, 10 + raw.first_samp / raw.info['sfreq']],
                        [10, 10], ['test', 'test'], raw.info['meas_date'])
    with pytest.warns(RuntimeWarning, match='outside data range'):
        raw.set_annotations(annot)

    # Color setting
    with pytest.raises(KeyError, match='must be strictly positive, or -1'):
        raw.plot(event_color={0: 'r'})
    with pytest.raises(TypeError, match='event_color key must be an int, got'):
        raw.plot(event_color={'foo': 'r'})
    plot_raw(raw, events=events, event_color={-1: 'r', 998: 'b'})


@pytest.mark.parametrize('group_by', ('position', 'selection'))
def test_plot_raw_groupby(raw, browser_backend, group_by):
    """Test group-by plotting of raw data."""
    with raw.info._unlock():
        raw.info['lowpass'] = 10.  # allow heavy decim during plotting
    order = (np.arange(len(raw.ch_names))[::-3] if group_by == 'position' else
             [1, 2, 4, 6])
    fig = raw.plot(group_by=group_by, order=order)
    x = fig.mne.traces[0].get_xdata()[10]
    y = fig.mne.traces[0].get_ydata()[10]
    fig._fake_keypress('down')  # change selection
    fig._fake_click((x, y), xform='data')  # mark bad
    fig._fake_click((0.5, 0.5), ax=fig.mne.ax_vscroll)  # change channels
    if browser_backend.name == 'matplotlib':
        # Test lasso-selection
        # (test difficult with Qt-backend, set plot_raw_selection)
        sel_fig = fig.mne.fig_selection
        topo_ax = sel_fig.mne.sensor_ax
        fig._fake_click([-0.425, 0.20223853], fig=sel_fig, ax=topo_ax,
                        xform='data')
        fig._fake_click((-0.5, 0.), add_points=[(0.5, 0.),
                                                (0.5, 0.5),
                                                (-0.5, 0.5)],
                        fig=sel_fig, ax=topo_ax, xform='data', kind='drag')
        fig._fake_keypress('down')
        fig._fake_keypress('up')
    fig._fake_keypress('up')
    fig._fake_scroll(0.5, 0.5, -1)  # scroll down
    fig._fake_scroll(0.5, 0.5, 1)  # scroll up


def test_plot_raw_meas_date(raw, browser_backend):
    """Test effect of mismatched meas_date in raw.plot()."""
    raw.set_meas_date(_dt_to_stamp(raw.info['meas_date'])[0])
    annot = Annotations([1 + raw.first_samp / raw.info['sfreq']], [5], ['bad'])
    with pytest.warns(RuntimeWarning, match='outside data range'):
        raw.set_annotations(annot)
    with _record_warnings():  # sometimes projection
        raw.plot(group_by='position', order=np.arange(8))
    fig = raw.plot()
    for key in ['down', 'up', 'escape']:
        fig._fake_keypress(key, fig=fig.mne.fig_selection)


def test_plot_raw_nan(raw, browser_backend):
    """Test plotting all NaNs."""
    raw._data[:] = np.nan
    # this should (at least) not die, the output should pretty clearly show
    # that there is a problem so probably okay to just plot something blank
    with _record_warnings():
        raw.plot(scalings='auto')


@testing.requires_testing_data
def test_plot_raw_white(raw_orig, noise_cov_io, browser_backend):
    """Test plotting whitened raw data."""
    raw_orig.crop(0, 1)
    fig = raw_orig.plot(noise_cov=noise_cov_io)
    # toggle whitening
    fig._fake_keypress('w')
    fig._fake_keypress('w')


@testing.requires_testing_data
def test_plot_ref_meg(raw_ctf, browser_backend):
    """Test plotting ref_meg."""
    raw_ctf.crop(0, 1)
    raw_ctf.plot()
    pytest.raises(ValueError, raw_ctf.plot, group_by='selection')


def test_plot_misc_auto(browser_backend):
    """Test plotting of data with misc auto scaling."""
    data = np.random.RandomState(0).randn(1, 1000)
    raw = RawArray(data, create_info(1, 1000., 'misc'))
    raw.plot()
    raw = RawArray(data, create_info(1, 1000., 'dipole'))
    raw.plot(order=[0])  # plot, even though it's not "data"
    browser_backend._close_all()


@pytest.mark.slowtest
def test_plot_annotations(raw, browser_backend):
    """Test annotation mode of the plotter."""
    ismpl = browser_backend.name == 'matplotlib'
    with raw.info._unlock():
        raw.info['lowpass'] = 10.
    _annotation_helper(raw, browser_backend)
    _annotation_helper(raw, browser_backend, events=True)

    annot = Annotations([42], [1], 'test', raw.info['meas_date'])
    with pytest.warns(RuntimeWarning, match='expanding outside'):
        raw.set_annotations(annot)
    _annotation_helper(raw, browser_backend)
    # test annotation visibility toggle
    fig = raw.plot()
    if ismpl:
        assert len(fig.mne.annotations) == 1
        assert len(fig.mne.annotation_texts) == 1
    else:
        assert len(fig.mne.regions) == 1
    fig._fake_keypress('a')  # start annotation mode
    if ismpl:
        checkboxes = fig.mne.show_hide_annotation_checkboxes
        checkboxes.set_active(0)
        assert len(fig.mne.annotations) == 0
        assert len(fig.mne.annotation_texts) == 0
        checkboxes.set_active(0)
        assert len(fig.mne.annotations) == 1
        assert len(fig.mne.annotation_texts) == 1
    else:
        fig.mne.visible_annotations['test'] = False
        fig._update_regions_visible()
        assert not fig.mne.regions[0].isVisible()
        fig.mne.visible_annotations['test'] = True
        fig._update_regions_visible()
        assert fig.mne.regions[0].isVisible()


@pytest.mark.parametrize('hide_which', ([], [0], [1], [0, 1]))
def test_remove_annotations(raw, hide_which, browser_backend):
    """Test that right-click doesn't remove hidden annotation spans."""
    descriptions = ['foo', 'bar']
    ann = Annotations(onset=[2, 1], duration=[1, 3],
                      description=descriptions)
    raw.set_annotations(ann)
    assert len(raw.annotations) == 2
    fig = raw.plot()
    fig._fake_keypress('a')  # start annotation mode
    if browser_backend.name == 'matplotlib':
        checkboxes = fig.mne.show_hide_annotation_checkboxes
        for which in hide_which:
            checkboxes.set_active(which)
    else:
        for hide_idx in hide_which:
            hide_key = descriptions[hide_idx]
            fig.mne.visible_annotations[hide_key] = False
        fig._update_regions_visible()
    fig._fake_click((2.5, 0.1), xform='data', button=3)
    assert len(raw.annotations) == len(hide_which)


@pytest.mark.parametrize('filtorder', (0, 2))  # FIR, IIR
def test_plot_raw_filtered(filtorder, raw, browser_backend):
    """Test filtering of raw plots."""
    # Opening that many plots can cause a Segmentation fault
    # if multithreading is activated in Qt-backend
    pg_kwargs = {'precompute': False}
    with pytest.raises(ValueError, match='lowpass.*Nyquist'):
        raw.plot(lowpass=raw.info['sfreq'] / 2., filtorder=filtorder,
                 **pg_kwargs)
    with pytest.raises(ValueError, match='highpass must be > 0'):
        raw.plot(highpass=0, filtorder=filtorder, **pg_kwargs)
    with pytest.raises(ValueError, match='Filter order must be'):
        raw.plot(lowpass=1, filtorder=-1, **pg_kwargs)
    with pytest.raises(ValueError, match="Invalid value for the 'clipping'"):
        raw.plot(clipping='foo', **pg_kwargs)
    raw.plot(lowpass=40, clipping='transparent', filtorder=filtorder,
             **pg_kwargs)
    raw.plot(highpass=1, clipping='clamp', filtorder=filtorder, **pg_kwargs)
    raw.plot(lowpass=40, butterfly=True, filtorder=filtorder, **pg_kwargs)
    # shouldn't break if all shown are non-data
    RawArray(np.zeros((1, 100)), create_info(1, 20., 'stim')).plot(lowpass=5)


def test_plot_raw_psd(raw, raw_orig):
    """Test plotting of raw psds."""
    raw_unchanged = raw.copy()
    # normal mode
    fig = raw.plot_psd(average=False)
    fig.canvas.callbacks.process(
        'resize_event',
        backend_bases.ResizeEvent('resize_event', fig.canvas))
    # specific mode
    picks = pick_types(raw.info, meg='mag', eeg=False)[:4]
    raw.plot_psd(tmax=None, picks=picks, area_mode='range', average=False,
                 spatial_colors=True)
    raw.plot_psd(tmax=20., color='yellow', dB=False, line_alpha=0.4,
                 average=False)
    plt.close('all')
    # one axes supplied
    ax = plt.axes()
    raw.plot_psd(tmax=None, picks=picks, ax=ax, average=True)
    plt.close('all')
    # two axes supplied
    _, axs = plt.subplots(2)
    raw.plot_psd(tmax=None, ax=axs, average=True)
    plt.close('all')
    # need 2, got 1
    ax = plt.axes()
    with pytest.raises(ValueError, match='of length 2.*the length is 1'):
        raw.plot_psd(ax=ax, average=True)
    plt.close('all')
    # topo psd
    ax = plt.subplot()
    raw.plot_psd_topo(axes=ax)
    plt.close('all')
    # with channel information not available
    for idx in range(len(raw.info['chs'])):
        raw.info['chs'][idx]['loc'] = np.zeros(12)
    with pytest.warns(RuntimeWarning, match='locations not available'):
        raw.plot_psd(spatial_colors=True, average=False)
    # with a flat channel
    raw[5, :] = 0
    for dB, estimate in itertools.product((True, False),
                                          ('power', 'amplitude')):
        with pytest.warns(UserWarning, match='[Infinite|Zero]'):
            fig = raw.plot_psd(average=True, dB=dB, estimate=estimate)
        # check grad axes
        title = fig.axes[0].get_title()
        ylabel = fig.axes[0].get_ylabel()
        ends_dB = ylabel.endswith('mathrm{(dB)}$')
        unit = '(fT/cm)²/Hz' if estimate == 'power' else r'fT/cm/\sqrt{Hz}'
        assert title == 'Gradiometers', title
        assert unit in ylabel, ylabel
        if dB:
            assert ends_dB, ylabel
        else:
            assert not ends_dB, ylabel
        # check mag axes
        title = fig.axes[1].get_title()
        ylabel = fig.axes[1].get_ylabel()
        unit = 'fT²/Hz' if estimate == 'power' else r'fT/\sqrt{Hz}'
        assert title == 'Magnetometers', title
        assert unit in ylabel, ylabel
    # test reject_by_annotation
    raw = raw_unchanged
    raw.set_annotations(Annotations([1, 5], [3, 3], ['test', 'test']))
    raw.plot_psd(reject_by_annotation=True)
    raw.plot_psd(reject_by_annotation=False)
    plt.close('all')

    # test fmax value checking
    with pytest.raises(ValueError, match='must not exceed ½ the sampling'):
        raw.plot_psd(fmax=50000)

    # test xscale value checking
    with pytest.raises(ValueError, match="Invalid value for the 'xscale'"):
        raw.plot_psd(xscale='blah')

    # gh-5046
    raw = raw_orig.crop(0, 1)
    picks = pick_types(raw.info, meg=True)
    raw.plot_psd(picks=picks, average=False)
    raw.plot_psd(picks=picks, average=True)
    plt.close('all')
    raw.set_channel_types({'MEG 0113': 'hbo', 'MEG 0112': 'hbr',
                           'MEG 0122': 'fnirs_cw_amplitude',
                           'MEG 0123': 'fnirs_od'},
                          verbose='error')
    fig = raw.plot_psd()
    assert len(fig.axes) == 10
    plt.close('all')

    # gh-7631
    n_times = sfreq = n_fft = 100
    data = 1e-3 * np.random.rand(2, n_times)
    info = create_info(['CH1', 'CH2'], sfreq)  # ch_types defaults to 'misc'
    raw = RawArray(data, info)
    picks = pick_types(raw.info, misc=True)
    raw.plot_psd(picks=picks, spatial_colors=False, n_fft=n_fft)
    plt.close('all')


def test_plot_sensors(raw):
    """Test plotting of sensor array."""
    plt.close('all')
    fig = raw.plot_sensors('3d')
    _fake_click(fig, fig.gca(), (-0.08, 0.67))
    raw.plot_sensors('topomap', ch_type='mag',
                     show_names=['MEG 0111', 'MEG 0131'])
    plt.close('all')
    ax = plt.subplot(111)
    raw.plot_sensors(ch_groups='position', axes=ax)
    raw.plot_sensors(ch_groups='selection', to_sphere=False)
    raw.plot_sensors(ch_groups=[[0, 1, 2], [3, 4]])
    pytest.raises(ValueError, raw.plot_sensors, ch_groups='asd')
    pytest.raises(TypeError, plot_sensors, raw)  # needs to be info
    pytest.raises(ValueError, plot_sensors, raw.info, kind='sasaasd')
    plt.close('all')
    fig, sels = raw.plot_sensors('select', show_names=True)
    ax = fig.axes[0]

    # Click with no sensors
    _fake_click(fig, ax, (0., 0.), xform='data')
    _fake_click(fig, ax, (0, 0.), xform='data', kind='release')
    assert fig.lasso.selection == []

    # Lasso with 1 sensor (upper left)
    _fake_click(fig, ax, (0, 1), xform='ax')
    fig.canvas.draw()
    assert fig.lasso.selection == []
    _fake_click(fig, ax, (0.65, 1), xform='ax', kind='motion')
    _fake_click(fig, ax, (0.65, 0.7), xform='ax', kind='motion')
    _fake_keypress(fig, 'control')
    _fake_click(fig, ax, (0, 0.7), xform='ax', kind='release', key='control')
    assert fig.lasso.selection == ['MEG 0121']

    # check that point appearance changes
    fc = fig.lasso.collection.get_facecolors()
    ec = fig.lasso.collection.get_edgecolors()
    assert (fc[:, -1] == [0.5, 1., 0.5]).all()
    assert (ec[:, -1] == [0.25, 1., 0.25]).all()

    _fake_click(fig, ax, (0.7, 1), xform='ax', kind='motion', key='control')
    xy = ax.collections[0].get_offsets()
    _fake_click(fig, ax, xy[2], xform='data', key='control')  # single sel
    assert fig.lasso.selection == ['MEG 0121', 'MEG 0131']
    _fake_click(fig, ax, xy[2], xform='data', key='control')  # deselect
    assert fig.lasso.selection == ['MEG 0121']
    plt.close('all')

    raw.info['dev_head_t'] = None  # like empty room
    with pytest.warns(RuntimeWarning, match='identity'):
        raw.plot_sensors()

    # Test plotting with sphere='eeglab'
    info = create_info(
        ch_names=['Fpz', 'Oz', 'T7', 'T8'],
        sfreq=100,
        ch_types='eeg'
    )
    data = 1e-6 * np.random.rand(4, 100)
    raw_eeg = RawArray(data=data, info=info)
    raw_eeg.set_montage('biosemi64')
    raw_eeg.plot_sensors(sphere='eeglab')

    # Should work with "FPz" as well
    raw_eeg.rename_channels({'Fpz': 'FPz'})
    raw_eeg.plot_sensors(sphere='eeglab')

    # Should still work without Fpz/FPz, as long as we still have Oz
    raw_eeg.drop_channels('FPz')
    raw_eeg.plot_sensors(sphere='eeglab')

    # Should raise if Oz is missing too, as we cannot reconstruct Fpz anymore
    raw_eeg.drop_channels('Oz')
    with pytest.raises(ValueError, match='could not find: Fpz'):
        raw_eeg.plot_sensors(sphere='eeglab')

    # Should raise if we don't have a montage
    chs = deepcopy(raw_eeg.info['chs'])
    raw_eeg.set_montage(None)
    with raw_eeg.info._unlock():
        raw_eeg.info['chs'] = chs
    with pytest.raises(ValueError, match='No montage was set'):
        raw_eeg.plot_sensors(sphere='eeglab')


@pytest.mark.parametrize('cfg_value', (None, '0.1,0.1'))
def test_min_window_size(raw, cfg_value, browser_backend):
    """Test minimum window plot size."""
    old_cfg = get_config('MNE_BROWSE_RAW_SIZE')
    set_config('MNE_BROWSE_RAW_SIZE', cfg_value)
    fig = raw.plot()
    # For an unknown reason, the Windows-CI is a bit off
    # (on local Windows 10 the size is exactly as expected).
    atol = 0 if not os.name == 'nt' else 0.2
    # 8 × 8 inches is default minimum size.
    assert_allclose(fig._get_size(), (8, 8), atol=atol)
    set_config('MNE_BROWSE_RAW_SIZE', old_cfg)


def test_scalings_int(browser_backend):
    """Test that auto scalings access samples using integers."""
    raw = RawArray(np.zeros((1, 500)), create_info(1, 1000., 'eeg'))
    raw.plot(scalings='auto')


@pytest.mark.parametrize('dur, n_dec', [(20, 1), (1.8, 2), (0.01, 4)])
def test_clock_xticks(raw, dur, n_dec, browser_backend):
    """Test if decimal seconds of xticks have appropriate length."""
    fig = raw.plot(duration=dur, time_format='clock')
    fig._redraw()
    tick_texts = fig._get_ticklabels('x')
    assert tick_texts[0].startswith('19:01:53')
    if len(tick_texts[0].split('.')) > 1:
        assert len(tick_texts[0].split('.')[1]) == n_dec


def test_plotting_order_consistency():
    """Test that our internal variables have some consistency."""
    pick_data_set = set(_PICK_TYPES_DATA_DICT)
    pick_data_set.remove('meg')
    pick_data_set.remove('fnirs')
    missing = pick_data_set.difference(set(_DATA_CH_TYPES_ORDER_DEFAULT))
    assert missing == set()


def test_plotting_temperature_gsr(browser_backend):
    """Test that we can plot temperature and GSR."""
    data = np.random.RandomState(0).randn(2, 1000)
    data[0] += 37  # deg C
    # no idea what the scale should be for GSR
    info = create_info(2, 1000., ['temperature', 'gsr'])
    raw = RawArray(data, info)
    fig = raw.plot()
    tick_texts = fig._get_ticklabels('y')
    assert len(tick_texts) == 2


@pytest.mark.pgtest
def test_plotting_memory_garbage_collection(raw, pg_backend):
    """Test that memory can be garbage collected properly."""
    pytest.importorskip('mne_qt_browser', minversion='0.4')
    raw.plot().close()
    import mne_qt_browser
    from mne_qt_browser._pg_figure import MNEQtBrowser
    assert len(mne_qt_browser._browser_instances) == 0
    _assert_no_instances(MNEQtBrowser, 'after closing')