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 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252
|
# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import itertools
import os
from copy import deepcopy
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pytest
from matplotlib import backend_bases
from numpy.testing import assert_allclose, assert_array_equal
import mne
from mne import Annotations, create_info, pick_types
from mne._fiff.pick import _DATA_CH_TYPES_ORDER_DEFAULT, _PICK_TYPES_DATA_DICT
from mne.annotations import _sync_onset
from mne.datasets import testing
from mne.io import RawArray
from mne.utils import (
_assert_no_instances,
_dt_to_stamp,
_record_warnings,
check_version,
get_config,
set_config,
)
from mne.viz import plot_raw, plot_sensors
from mne.viz.utils import _fake_click, _fake_keypress
base_dir = Path(__file__).parents[2] / "io" / "tests" / "data"
raw_fname = base_dir / "test_raw.fif"
def _get_button_xy(buttons, idx):
from mne.viz._mpl_figure import _OLD_BUTTONS
if _OLD_BUTTONS:
return buttons.circles[idx].center
else:
# Each transform is to display coords, and our offsets are in Axes
# coords. We want data coords, so we go Axes -> display -> data.
return buttons.ax.transData.inverted().transform(
buttons.ax.transAxes.transform(buttons.ax.collections[0].get_offsets()[idx])
)
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 = _get_button_xy(ann_fig.mne.radio_ax.buttons, ix)
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.0, 1.0), add_points=[(5.0, 1.0)], 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.0), **hover_kwargs) # well inside ann.
fig._fake_click((4.9, 1.0), **hover_kwargs) # almost at edge
assert fig.mne.annotation_hover_line is not None
fig._fake_click((5.5, 1.0), **hover_kwargs) # well outside ann.
assert fig.mne.annotation_hover_line is None
# more tests of hover line
fig._fake_click((4.6, 1.0), **hover_kwargs) # well inside ann.
fig._fake_click((4.9, 1.0), **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.0), **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.0, 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.0, atol=atol)
# modify annotation from end (duration 4 → 1.5)
fig._fake_click((4.9, 1.0), xform="data", button=1, kind="motion") # ease up to it
fig._fake_click(
(5.0, 1.0), add_points=[(2.5, 1.0)], 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.0, 1.0), add_points=[(0.5, 1.0)], 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.0), add_points=[(2.0, 1.0)], 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.0), 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 == "matplotlib" or browse_backend.name == "matplotlib":
return ssp_fig.mne.proj_checkboxes.get_status()
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":
text_lab = ssp_fig.mne.proj_checkboxes.labels[idx]
pos = np.mean(
text_lab.get_tightbbox(renderer=fig.canvas.get_renderer()), axis=0
)
fig._fake_click(
pos, fig=ssp_fig, ax=ssp_fig.mne.proj_checkboxes.ax, xform="pix"
)
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.0
t = np.arange(10000) / sfreq
data = np.sin(2 * np.pi * 10.0 * 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.0 # 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
left_temp = "Left-temporal"
sel_dict = fig.mne.ch_selections
assert len(fig.mne.traces) == len(sel_dict[left_temp]) # 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:
print(f"Clicking button: {repr(left_temp)}")
assert sel_fig.mne.radio_ax.buttons.labels[0].get_text() == left_temp
xy = _get_button_xy(sel_fig.mne.radio_ax.buttons, 0)
lim = sel_fig.mne.radio_ax.get_xlim()
assert lim[0] < xy[0] < lim[1]
lim = sel_fig.mne.radio_ax.get_ylim()
assert lim[0] < xy[1] < lim[1]
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 not fig.mne.butterfly
assert len(fig.mne.traces) == len(sel_dict[left_temp]) # 6
# test clicking on "custom" when not defined: should be no-op
if ismpl:
before_state = sel_fig.mne.radio_ax.buttons.value_selected
xy = _get_button_xy(sel_fig.mne.radio_ax.buttons, -1)
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_temp]) # 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.0 # 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.0 # 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.0 # 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.0 # allow heavy decim during plotting
assert raw.info["bads"] == []
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"})
# gh-12547
raw.info["bads"] = raw.ch_names[1:2]
picks = [1, 7, 5, 2, 3]
fig = raw.plot(events=events, order=picks, group_by="original")
assert_array_equal(fig.mne.picks, picks)
def test_plot_raw_picks(raw, browser_backend):
"""Test functionality of picks and order arguments."""
with raw.info._unlock():
raw.info["lowpass"] = 10.0 # allow heavy decim during plotting
fig = raw.plot(picks=["MEG 0112"])
assert len(fig.mne.traces) == 1
fig = raw.plot(picks=["meg"])
assert len(fig.mne.traces) == len(raw.get_channel_types(picks="meg"))
fig = raw.plot(order=[4, 3])
assert_array_equal(fig.mne.ch_order, np.array([4, 3]))
fig = raw.plot(picks=[4, 3])
assert_array_equal(fig.mne.ch_order, np.array([3, 4]))
@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.0 # 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.0),
add_points=[(0.5, 0.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.0, "misc"))
raw.plot()
raw = RawArray(data, create_info(1, 1000.0, "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.0
_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()
# Check if single annotation toggle works
ch_pick = fig.mne.inst.ch_names[0]
fig._toggle_single_channel_annotation(ch_pick, 0)
assert fig.mne.inst.annotations.ch_names[0] == (ch_pick,)
@pytest.mark.parametrize("active_annot_idx", (0, 1, 2))
def test_overlapping_annotation_deletion(raw, browser_backend, active_annot_idx):
"""Test deletion of annotations via right-click."""
ismpl = browser_backend.name == "matplotlib"
if not ismpl and not check_version("mne_qt_browser", "0.5.2"):
pytest.xfail("Old mne-qt-browser")
with raw.info._unlock():
raw.info["lowpass"] = 10.0
annot_labels = list("abc")
# the test applies to the middle three annotations; those before and after are
# there to ensure our bookkeeping works
annot = Annotations(
onset=[3, 3.4, 3.7, 13, 13.4, 13.7, 19, 19.4, 19.7],
duration=[2, 1, 3] * 3,
description=annot_labels * 3,
)
raw.set_annotations(annot)
start = 10
duration = 8
fig = raw.plot(start=start, duration=duration)
def _get_visible_labels(fig_dot_mne):
if ismpl:
# MPL backend's `fig.mne.annotation_texts` → only the visible ones
visible_labels = [x.get_text() for x in fig_dot_mne.annotation_texts]
else:
# PyQtGraph backend's `fig.mne.regions` → all annots (even offscreen ones)
# so we need to (1) get annotations from fig.mne.inst, and (2) compute
# ourselves which ones are visible.
_annot = fig_dot_mne.inst.annotations
_start = start + fig_dot_mne.inst.first_time
_end = _start + duration
visible_indices = np.nonzero(
np.logical_and(_annot.onset > _start, _annot.onset < _end)
)
visible_labels = np.array(
[x.label_item.toPlainText() for x in fig_dot_mne.regions]
)[visible_indices].tolist()
return visible_labels
assert annot_labels == _get_visible_labels(fig.mne)
fig._fake_keypress("a") # start annotation mode
if ismpl:
buttons = fig.mne.fig_annotation.mne.radio_ax.buttons
buttons.set_active(active_annot_idx)
current_active = buttons.value_selected
else:
buttons = fig.mne.fig_annotation.description_cmbx
buttons.setCurrentIndex(active_annot_idx)
current_active = buttons.currentText()
assert current_active == annot_labels[active_annot_idx]
# x value of 14 is in area that overlaps all 3 visible annotations
fig._fake_click((14, 1.0), xform="data", button=3)
expected = set(annot_labels) - set(annot_labels[active_annot_idx])
assert expected == set(_get_visible_labels(fig.mne))
@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()
# always click twice: should not affect hidden annotation spans
for _ in descriptions:
fig._fake_click((2.5, 0.1), xform="data", button=3)
assert len(raw.annotations) == len(hide_which)
def test_merge_annotations(raw, browser_backend):
"""Test merging of annotations in the Qt backend.
Let's not bother in figuring out on which sample the _fake_click actually
dropped the annotation, especially with the 600.614 Hz weird sampling rate.
-> atol = 10 / raw.info["sfreq"]
"""
if browser_backend.name == "matplotlib":
pytest.skip("The MPL backend does not support draggable annotations.")
elif not check_version("mne_qt_browser", "0.5.3"):
pytest.xfail("mne_qt_browser < 0.5.3 does not merge annotations properly")
annot = Annotations(
onset=[1, 3, 4, 5, 7, 8],
duration=[1, 0.5, 0.8, 1, 0.5, 0.5],
description=["bad_test", "bad_test", "bad_test", "test", "test", "test"],
)
raw.set_annotations(annot)
fig = raw.plot()
fig._fake_keypress("a") # start annotation mode
assert len(raw.annotations) == 6
assert_allclose(
raw.annotations.onset,
np.array([1, 3, 4, 5, 7, 8]) + raw.first_samp / raw.info["sfreq"],
atol=10 / raw.info["sfreq"],
)
# drag edge and merge 2 annotations in focus (selected description)
fig._fake_click(
(3.5, 1.0), add_points=[(4.2, 1.0)], xform="data", button=1, kind="drag"
)
assert len(raw.annotations) == 5
assert_allclose(
raw.annotations.onset,
np.array([1, 3, 5, 7, 8]) + raw.first_samp / raw.info["sfreq"],
atol=10 / raw.info["sfreq"],
)
assert_allclose(
raw.annotations.duration,
np.array([1, 1.8, 1, 0.5, 0.5]),
atol=10 / raw.info["sfreq"],
)
# drag annotation and merge 2 annotations in focus (selected description)
fig._fake_click(
(1.5, 1.0), add_points=[(3, 1.0)], xform="data", button=1, kind="drag"
)
assert len(raw.annotations) == 4
assert_allclose(
raw.annotations.onset,
np.array([2.5, 5, 7, 8]) + raw.first_samp / raw.info["sfreq"],
atol=10 / raw.info["sfreq"],
)
assert_allclose(
raw.annotations.duration,
np.array([2.3, 1, 0.5, 0.5]),
atol=10 / raw.info["sfreq"],
)
# drag edge and merge 2 annotations not in focus
fig._fake_click(
(7.5, 1.0), add_points=[(8.2, 1.0)], xform="data", button=1, kind="drag"
)
assert len(raw.annotations) == 3
assert_allclose(
raw.annotations.onset,
np.array([2.5, 5, 7]) + raw.first_samp / raw.info["sfreq"],
atol=10 / raw.info["sfreq"],
)
assert_allclose(
raw.annotations.duration, np.array([2.3, 1, 1.5]), atol=10 / raw.info["sfreq"]
)
# drag annotation and merge 2 annotations not in focus
fig._fake_click(
(5.6, 1.0), add_points=[(7.2, 1.0)], xform="data", button=1, kind="drag"
)
assert len(raw.annotations) == 2
assert_allclose(
raw.annotations.onset,
np.array([2.5, 6.6]) + raw.first_samp / raw.info["sfreq"],
atol=10 / raw.info["sfreq"],
)
assert_allclose(
raw.annotations.duration, np.array([2.3, 1.9]), atol=10 / raw.info["sfreq"]
)
@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.0, 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.0, "stim")).plot(lowpass=5)
def test_plot_raw_psd(raw, raw_orig):
"""Test plotting of raw psds."""
raw_unchanged = raw.copy()
spectrum = raw.compute_psd()
# deprecation change handler
old_defaults = dict(picks="data", exclude="bads")
fig = spectrum.plot(average=False, amplitude=False)
# normal mode
fig = spectrum.plot(average=False, amplitude=False, **old_defaults)
fig.canvas.callbacks.process(
"resize_event", backend_bases.ResizeEvent("resize_event", fig.canvas)
)
# specific mode
picks = pick_types(spectrum.info, meg="mag", eeg=False)[:4]
spectrum.plot(
picks=picks, ci="range", spatial_colors=True, exclude="bads", amplitude=False
)
raw.compute_psd(tmax=20.0).plot(
color="yellow", dB=False, alpha=0.4, amplitude=True, **old_defaults
)
plt.close("all")
# one axes supplied
ax = plt.axes()
spectrum.plot(picks=picks, axes=ax, average=True, exclude="bads", amplitude=False)
plt.close("all")
# two axes supplied
_, axs = plt.subplots(2)
spectrum.plot(axes=axs, average=True, amplitude=False, **old_defaults)
plt.close("all")
# need 2, got 1
ax = plt.axes()
with pytest.raises(ValueError, match="of length 2.*the length is 1"):
spectrum.plot(axes=ax, average=True, amplitude=False, **old_defaults)
plt.close("all")
# topo psd
ax = plt.subplot()
spectrum.plot_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 (
_record_warnings(),
pytest.warns(RuntimeWarning, match="locations not available"),
):
raw.compute_psd().plot(spatial_colors=True, average=False)
# with a flat channel
raw[5, :] = 0
with pytest.warns(UserWarning, match="[Infinite|Zero]"):
spectrum = raw.compute_psd()
for dB, amplitude in itertools.product((True, False), (True, False)):
with pytest.warns(UserWarning, match="[Infinite|Zero]"):
fig = spectrum.plot(average=True, dB=dB, amplitude=amplitude)
# check grad axes
title = fig.axes[0].get_title()
ylabel = fig.axes[0].get_ylabel()
unit = r"fT/cm/\sqrt{Hz}" if amplitude else "(fT/cm)²/Hz"
assert title == "Gradiometers", title
assert unit in ylabel, ylabel
if dB:
assert "dB" in ylabel
else:
assert "dB" not in ylabel
# check mag axes
title = fig.axes[1].get_title()
ylabel = fig.axes[1].get_ylabel()
unit = r"fT/\sqrt{Hz}" if amplitude else "fT²/Hz"
assert title == "Magnetometers", title
assert unit in ylabel, ylabel
# test xscale value checking
raw = raw_unchanged
spectrum = raw.compute_psd()
with pytest.raises(ValueError, match="Invalid value for the 'xscale'"):
spectrum.plot(xscale="blah")
# gh-5046
raw = raw_orig.crop(0, 1)
picks = pick_types(raw.info, meg=True)
spectrum = raw.compute_psd(picks=picks)
spectrum.plot(average=False, amplitude=False, **old_defaults)
spectrum.plot(average=True, amplitude=False, **old_defaults)
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.compute_psd().plot(amplitude=False, **old_defaults)
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)
spectrum = raw.compute_psd(picks=picks, n_fft=n_fft)
spectrum.plot(spatial_colors=False, picks=picks, exclude="bads", amplitude=False)
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]])
raw.plot_sensors(ch_groups=np.array([[0, 1, 2], [3, 4, 5]]))
raw.plot_sensors(ch_groups=np.array([[0, 1, 2], [3, 4]], dtype=object))
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, 0.0), xform="data")
_fake_click(fig, ax, (0, 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, 0.5]).all()
assert (ec[:, -1] == [0.25, 1.0, 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.0, "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")
pick_data_set.remove("eyetrack")
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.0, ["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")
def test_plotting_scalebars(browser_backend, qtbot):
"""Test that raw scalebars are not overplotted."""
ismpl = browser_backend.name == "matplotlib"
raw = mne.io.read_raw_fif(raw_fname).crop(0, 1).load_data()
fig = raw.plot(butterfly=True)
if ismpl:
ch_types = [text.get_text() for text in fig.mne.ax_main.get_yticklabels()]
assert ch_types == ["mag", "grad", "eeg", "eog", "stim"]
delta = 0.25
offset = 0
else:
qtbot.wait_exposed(fig)
for _ in range(10):
ch_types = list(fig.mne.channel_axis.ch_texts) # keys
if len(ch_types) > 0:
break
qtbot.wait(100) # pragma: no cover
# the grad/mag difference here is intentional in _pg_figure.py
assert ch_types == ["grad", "mag", "eeg", "eog", "stim"]
delta = 0.5 # TODO: Probably should also be 0.25?
offset = 1
assert ch_types.pop(-1) == "stim"
for ci, ch_type in enumerate(ch_types, offset):
err_msg = f"{ch_type=} should be centered around y={ci}"
this_bar = fig.mne.scalebars[ch_type]
if ismpl:
yvals = this_bar.get_data()[1]
else:
yvals = this_bar.get_ydata()
assert_allclose(yvals, [ci - delta, ci + delta], err_msg=err_msg)
|