Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/dev/14270.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The :class:`mne.viz.Brain` GUI now gives label-mode traces the same as vertex traces, and autoscales the traces plot's y-axis whenever a new trace is picked, by `Payam Sadeghi-Shabestari`_.
57 changes: 48 additions & 9 deletions mne/viz/_brain/_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
self._peak_vertices = {}
self._auto_peak_points = set()
self._trace_meta = {}
self._label_trace_meta = {}
self._mouse_no_mvt = -1
self._show_hover_info = False
self._hover_caption = None
Expand Down Expand Up @@ -1659,6 +1660,7 @@ def _remove_label_glyph(self, hemi, label_id):
# subsequent removal (and clear_glyphs at annotation changes) fail too
self._picked_patches[hemi].remove(label_id)
line, label._line = label._line, None
self._label_trace_meta.pop(line, None)
if line is not None:
try:
line.remove()
Expand Down Expand Up @@ -1789,16 +1791,42 @@ def _trace_display_label(self, line):
The vertex auto-picked at peak activation for each hemisphere gets a
"Peak (LH) 1000"-style name; other picked vertices get a compact
"LH 1000"-style name instead of the full MNI-coordinate string (still
available as the row's tooltip). RMS curves are returned unchanged.
available as the row's tooltip). A picked parcellation label gets its
display name (e.g. "Superiortemporal (LH)") instead of the raw
internal name (still available as the tooltip). RMS curves are
returned unchanged.
"""
meta = self._trace_meta.get(line)
if meta is None:
return line.get_label()
hemi, vertex_id, _ = meta
hemi_names = {"lh": "LH", "rh": "RH", "vol": "Vol"}
if self._peak_vertices.get(hemi) == vertex_id:
return f"Peak ({hemi_names[hemi]}) {vertex_id}"
return f"{hemi_names[hemi]} {vertex_id}"
if meta is not None:
hemi, vertex_id, _ = meta
hemi_names = {"lh": "LH", "rh": "RH", "vol": "Vol"}
if self._peak_vertices.get(hemi) == vertex_id:
return f"Peak ({hemi_names[hemi]}) {vertex_id}"
return f"{hemi_names[hemi]} {vertex_id}"
label_meta = self._label_trace_meta.get(line)
if label_meta is not None:
hemi, label_name, _, _ = label_meta
hemi_names = {"lh": "LH", "rh": "RH"}
display_name = label_name
for suffix in ("-lh", "-rh", "_lh", "_rh"):
if display_name.endswith(suffix):
display_name = display_name[: -len(suffix)]
break
display_name = display_name.replace("_", " ").replace("-", " ").title()
return f"{display_name} ({hemi_names.get(hemi, hemi)})"
return line.get_label()

def _trace_display_subtitle(self, line):
"""Return an optional small subtitle line for a trace-list row."""
meta = self._trace_meta.get(line)
if meta is not None:
mni_str = meta[2]
return f"MNI: {mni_str}" if mni_str else None
label_meta = self._label_trace_meta.get(line)
if label_meta is not None:
_, _, mode, n_vertices = label_meta
return f"{n_vertices} vertices, mode: {mode}"
return None

def clear_glyphs(self):
"""Clear the picking glyphs."""
Expand Down Expand Up @@ -1879,6 +1907,8 @@ def plot_time_course(self, hemi, vertex_id, color, update=True):
)
self._trace_meta[line] = (hemi, vertex_id, mni_str)
if update:
self.mpl_canvas.axes.relim()
self.mpl_canvas.axes.autoscale_view()
self.mpl_canvas.update_plot()
return line

Expand Down Expand Up @@ -2671,8 +2701,17 @@ def add_label(
tc = np.linalg.norm(tc, axis=0)
color = next(self.color_cycle)
line = self.mpl_canvas.plot(
self._data["time"], tc, label=label_name, color=color
self._data["time"], tc, label=label_name, color=color, update=False
)
self._label_trace_meta[line] = (
hemi,
label_name,
self.label_extract_mode,
len(label.vertices),
)
self.mpl_canvas.axes.relim()
self.mpl_canvas.axes.autoscale_view()
self.mpl_canvas.update_plot()
else:
line = None

Expand Down
26 changes: 26 additions & 0 deletions mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1540,6 +1540,12 @@ def row_text(row):
assert str(vertex_id) in line.get_label()
assert row_text(row) == f"LH {vertex_id}"

# the y-axis must autoscale to fit a newly-picked trace, not leave it
# clipped outside whatever range the previous traces happened to set
ymin, ymax = canvas.axes.get_ylim()
y = line.get_ydata()
assert y.min() >= ymin and y.max() <= ymax

# toggling a row hides the trace and its 3D glyph together, without
# rebuilding the row list (sync() must skip unchanged trace sets --
# the whole point of the native list was to stop rebuilding on every
Expand Down Expand Up @@ -1840,9 +1846,29 @@ def test_brain_click_picking_label(renderer_interactive_pyvistaqt, brain_gc, qtb
assert len(brain._picked_patches["lh"]) == 0
QTest.mouseClick(widget, Qt.LeftButton, Qt.NoModifier, point)
assert len(brain._picked_patches["lh"]) == 1

# the picked label's trace-list row gets a friendly display name/subtitle
# instead of the raw internal label name (still available as the tooltip)
label_id = brain._picked_patches["lh"][0]
label = brain._annotation_labels["lh"][label_id]
line = label._line
assert line in brain._label_trace_meta
display_label = brain._trace_display_label(line)
assert display_label != line.get_label()
assert display_label.endswith("(LH)")
subtitle = brain._trace_display_subtitle(line)
assert str(brain.label_extract_mode) in subtitle
assert str(len(label.vertices)) in subtitle

# the y-axis must autoscale to fit the newly-picked label's trace
ymin, ymax = brain.mpl_canvas.axes.get_ylim()
y = line.get_ydata()
assert y.min() >= ymin and y.max() <= ymax

# clicking the same label again removes it
QTest.mouseClick(widget, Qt.LeftButton, Qt.NoModifier, point)
assert len(brain._picked_patches["lh"]) == 0
assert line not in brain._label_trace_meta
# the clear-glyphs shortcut clears a picked label
QTest.mouseClick(widget, Qt.LeftButton, Qt.NoModifier, point)
assert len(brain._picked_patches["lh"]) == 1
Expand Down
7 changes: 3 additions & 4 deletions mne/viz/backends/_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1501,10 +1501,9 @@ def __init__(self, canvas, line):
text.setWordWrap(True)
text_col.addWidget(text)

meta = brain._trace_meta.get(line) if brain else None
coords = meta[2] if meta is not None else None
if coords:
coord_label = QLabel(f"MNI: {coords}")
subtitle = brain._trace_display_subtitle(line) if brain else None
if subtitle:
coord_label = QLabel(subtitle)
coord_label.setStyleSheet(
"color: palette(placeholder-text); font-size: 8pt;"
)
Expand Down
Loading