From b232aeb7574a02a0fd59365f6aae70d3c2334696 Mon Sep 17 00:00:00 2001 From: payam Date: Thu, 3 Sep 2026 16:52:47 +0200 Subject: [PATCH 1/4] label trace now available with mode option --- mne/viz/_brain/_brain.py | 59 +++++++++++++++++++++++++++++++++------- mne/viz/backends/_qt.py | 7 ++--- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index b296fc0a446..0bc29955421 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -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 @@ -1055,7 +1056,7 @@ def _configure_dock_trace_widget(self, name): self._configure_vertex_time_course() return - layout = self._renderer._dock_add_group_box(name, collapse=True) + layout = self._renderer._dock_add_group_box(name, collapse=False) # setup candidate annots @safe_event @@ -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() @@ -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.""" @@ -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 @@ -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 diff --git a/mne/viz/backends/_qt.py b/mne/viz/backends/_qt.py index da433f0053e..b0da86e81f4 100644 --- a/mne/viz/backends/_qt.py +++ b/mne/viz/backends/_qt.py @@ -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;" ) From d26fd539579d5a0962bd50cd92ee785819373669 Mon Sep 17 00:00:00 2001 From: payam Date: Thu, 3 Sep 2026 16:57:44 +0200 Subject: [PATCH 2/4] couple of tests --- mne/viz/_brain/tests/test_brain.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/mne/viz/_brain/tests/test_brain.py b/mne/viz/_brain/tests/test_brain.py index 23673f9ad8e..a5ec6de667d 100644 --- a/mne/viz/_brain/tests/test_brain.py +++ b/mne/viz/_brain/tests/test_brain.py @@ -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 @@ -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 From e2221e50b5717b3e933115429a353d116ca6c01d Mon Sep 17 00:00:00 2001 From: payam Date: Thu, 3 Sep 2026 17:09:07 +0200 Subject: [PATCH 3/4] should be collapsed as others --- mne/viz/_brain/_brain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_brain/_brain.py b/mne/viz/_brain/_brain.py index 0bc29955421..dd594766ae1 100644 --- a/mne/viz/_brain/_brain.py +++ b/mne/viz/_brain/_brain.py @@ -1056,7 +1056,7 @@ def _configure_dock_trace_widget(self, name): self._configure_vertex_time_course() return - layout = self._renderer._dock_add_group_box(name, collapse=False) + layout = self._renderer._dock_add_group_box(name, collapse=True) # setup candidate annots @safe_event From 1d0a2bfaa1e051fad0dcb2087b94d494bb8f1fd4 Mon Sep 17 00:00:00 2001 From: payam Date: Fri, 4 Sep 2026 07:20:44 +0200 Subject: [PATCH 4/4] changelog --- doc/changes/dev/14270.newfeature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/14270.newfeature.rst diff --git a/doc/changes/dev/14270.newfeature.rst b/doc/changes/dev/14270.newfeature.rst new file mode 100644 index 00000000000..a2bd18b68c5 --- /dev/null +++ b/doc/changes/dev/14270.newfeature.rst @@ -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`_.