@@ -580,6 +580,8 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
580580 self .rms = None
581581 self ._picked_patches = {key : list () for key in all_keys }
582582 self ._picked_points = dict ()
583+ self ._peak_vertices = {}
584+ self ._trace_meta = {}
583585 self ._mouse_no_mvt = - 1
584586 self ._show_hover_info = False
585587 self ._hover_caption = None
@@ -614,6 +616,10 @@ def setup_time_viewer(self, time_viewer=True, show_traces=True):
614616 self .separate_canvas = False
615617 del show_traces
616618
619+ # Start with the first-added overlay active (the colormap dock's
620+ # default) so that the scalar bar, picking, and traces are all
621+ # configured against the same overlay
622+ self ._active_data_key = next (iter (self ._all_data ))
617623 self ._configure_time_label ()
618624 self ._configure_scalar_bar ()
619625 self ._configure_shortcuts ()
@@ -668,8 +674,9 @@ def _clean(self):
668674 self .plotter ._Iren = _FakeIren ()
669675 if getattr (self .plotter , "picker" , None ) is not None :
670676 self .plotter .picker = None
671- if getattr (self ._renderer , "_picker" , None ) is not None :
672- self ._renderer ._picker = None
677+ for picker in ("_picker" , "_hover_picker" ):
678+ if getattr (self ._renderer , picker , None ) is not None :
679+ setattr (self ._renderer , picker , None )
673680 # XXX end PyVista
674681 for key in (
675682 "plotter" ,
@@ -1044,6 +1051,7 @@ def _configure_dock_trace_widget(self, name):
10441051 layout = self ._renderer ._dock_add_group_box (name , collapse = True )
10451052
10461053 # setup candidate annots
1054+ @safe_event
10471055 @_auto_weakref
10481056 def _set_annot (annot ):
10491057 self .clear_glyphs ()
@@ -1060,6 +1068,7 @@ def _set_annot(annot):
10601068 self ._renderer ._update ()
10611069
10621070 # setup label extraction parameters
1071+ @safe_event
10631072 @_auto_weakref
10641073 def _set_label_mode (mode ):
10651074 if self .traces_mode != "label" :
@@ -1085,7 +1094,10 @@ def _set_label_mode(mode):
10851094 cands = cands + ["None" ]
10861095 self .annot = cands [0 ]
10871096 stc = self ._data ["stc" ]
1088- modes = _get_allowed_label_modes (stc )
1097+ # None (no extraction) is allowed by _get_allowed_label_modes but is
1098+ # not a valid choice here; with src=None it would otherwise end up
1099+ # last and become the default, breaking label extraction
1100+ modes = [m for m in _get_allowed_label_modes (stc ) if m is not None ]
10891101 if self ._data ["src" ] is None :
10901102 modes = [
10911103 m for m in modes if m not in self .default_label_extract_modes ["src" ]
@@ -1116,9 +1128,19 @@ def _configure_dock(self):
11161128 self ._configure_dock_colormap_widget (name = "Color Limits" )
11171129 self ._configure_dock_orientation_widget (name = "Orientation" )
11181130 self ._configure_dock_surface_widget (name = "Surface" )
1119- self ._configure_dock_trace_widget (name = "Trace" )
1131+ self ._configure_dock_trace_widget (name = "Atlas" )
1132+ self ._configure_dock_trace_list_widget (name = "Trace List" )
11201133 self ._renderer ._dock_finalize ()
11211134
1135+ def _configure_dock_trace_list_widget (self , name ):
1136+ if not self .show_traces or self .mpl_canvas is None :
1137+ return
1138+ add_trace_list = getattr (self ._renderer , "_dock_add_trace_list" , None )
1139+ if add_trace_list is None :
1140+ return
1141+ self .mpl_canvas ._trace_list = add_trace_list (name , collapse = False )
1142+ self .mpl_canvas .sync_traces ()
1143+
11221144 def _configure_mplcanvas (self ):
11231145 # Get the fractional components for the brain and mpl
11241146 self .mpl_canvas = self ._renderer ._window_get_mplcanvas (
@@ -1148,6 +1170,7 @@ def _configure_vertex_time_course(self):
11481170
11491171 # Plot one RMS curve per overlay so the viewer shows all overlays.
11501172 self .rms = []
1173+ self ._peak_vertices = {}
11511174 multi = len (self ._all_data ) > 1
11521175 for overlay_key , overlay_data in self ._all_data .items ():
11531176 y_parts = []
@@ -1170,12 +1193,11 @@ def _configure_vertex_time_course(self):
11701193 (line ,) = self .mpl_canvas .axes .plot (
11711194 overlay_data ["time" ],
11721195 rms ,
1173- lw = 3 ,
1196+ lw = 3.5 ,
11741197 label = label ,
11751198 zorder = 3 ,
11761199 color = next (self .color_cycle ),
11771200 alpha = 0.5 ,
1178- ls = ":" ,
11791201 )
11801202 self .rms .append (line )
11811203
@@ -1204,9 +1226,11 @@ def _configure_vertex_time_course(self):
12041226 ind = np .unravel_index (
12051227 np .argmax (np .abs (use_data ), axis = None ), use_data .shape
12061228 )
1229+ vertex_id = vertices [ind [0 ]]
1230+ self ._peak_vertices [hemi ] = vertex_id
12071231 publish (
12081232 self ,
1209- VertexSelect (hemi = hemi , vertex_id = vertices [ ind [ 0 ]] , source_id = ind [0 ]),
1233+ VertexSelect (hemi = hemi , vertex_id = vertex_id , source_id = ind [0 ]),
12101234 )
12111235
12121236 def _configure_picking (self ):
@@ -1260,7 +1284,7 @@ def _on_surface_hover(self, iren, event): # event == "MouseMoveEvent"
12601284
12611285 x , y = iren .GetEventPosition ()
12621286 picked_renderer = iren .FindPokedRenderer (x , y )
1263- vtk_picker = self ._renderer ._picker
1287+ vtk_picker = self ._renderer ._hover_picker
12641288 vtk_picker .Pick (x , y , 0 , picked_renderer )
12651289 cell_id = vtk_picker .GetCellId ()
12661290 mapper = vtk_picker .GetMapper ()
@@ -1579,11 +1603,19 @@ def _add_label_glyph(self, hemi, mesh, vertex_id):
15791603
15801604 def _remove_label_glyph (self , hemi , label_id ):
15811605 label = self ._annotation_labels [hemi ][label_id ]
1582- label ._line .remove ()
1606+ # do the bookkeeping first so that a failure partway cannot leave a
1607+ # picked label whose line is already detached, which would make every
1608+ # subsequent removal (and clear_glyphs at annotation changes) fail too
1609+ self ._picked_patches [hemi ].remove (label_id )
1610+ line , label ._line = label ._line , None
1611+ if line is not None :
1612+ try :
1613+ line .remove ()
1614+ except ValueError : # already detached from the axes
1615+ pass
15831616 self .color_cycle .restore (label ._color )
15841617 self .mpl_canvas .update_plot ()
15851618 self .layered_meshes [hemi ].remove_overlay (label .name )
1586- self ._picked_patches [hemi ].remove (label_id )
15871619
15881620 def _add_vertex_glyph (self , hemi , mesh , vertex_id , update = True ):
15891621 _ensure_int (vertex_id )
@@ -1659,6 +1691,7 @@ def _remove_vertex_glyph(self, *, hemi, vertex_id, render=True):
16591691 return
16601692 color , line = spheres [0 ]["color" ], spheres [0 ]["line" ]
16611693 line .remove ()
1694+ self ._trace_meta .pop (line , None )
16621695 self .mpl_canvas .update_plot ()
16631696
16641697 with warnings .catch_warnings (record = True ):
@@ -1672,6 +1705,42 @@ def _remove_vertex_glyph(self, *, hemi, vertex_id, render=True):
16721705 if render :
16731706 self ._renderer ._update ()
16741707
1708+ def _set_trace_visible (self , line , visible ):
1709+ """Toggle a trace's 3D glyph visibility to match its plot visibility."""
1710+ for spheres in self ._picked_points .values ():
1711+ if spheres [0 ]["line" ] is line :
1712+ for sphere in spheres :
1713+ sphere ["actor" ].SetVisibility (visible )
1714+ self ._renderer ._update ()
1715+ return
1716+
1717+ def _set_trace_highlight (self , line ):
1718+ """Dim the 3D glyphs of every picked trace except the highlighted one."""
1719+ if not self ._picked_points :
1720+ return
1721+ for spheres in self ._picked_points .values ():
1722+ opacity = 1.0 if line in (None , spheres [0 ]["line" ]) else 0.3
1723+ for sphere in spheres :
1724+ sphere ["actor" ].GetProperty ().SetOpacity (opacity )
1725+ self ._renderer ._update ()
1726+
1727+ def _trace_display_label (self , line ):
1728+ """Return a short, dock-friendly trace-list label.
1729+
1730+ The vertex auto-picked at peak activation for each hemisphere gets a
1731+ "Peak (LH) 1000"-style name; other picked vertices get a compact
1732+ "LH 1000"-style name instead of the full MNI-coordinate string (still
1733+ available as the row's tooltip). RMS curves are returned unchanged.
1734+ """
1735+ meta = self ._trace_meta .get (line )
1736+ if meta is None :
1737+ return line .get_label ()
1738+ hemi , vertex_id , _ = meta
1739+ hemi_names = {"lh" : "LH" , "rh" : "RH" , "vol" : "Vol" }
1740+ if self ._peak_vertices .get (hemi ) == vertex_id :
1741+ return f"Peak ({ hemi_names [hemi ]} ) { vertex_id } "
1742+ return f"{ hemi_names [hemi ]} { vertex_id } "
1743+
16751744 def clear_glyphs (self ):
16761745 """Clear the picking glyphs."""
16771746 if not self .time_viewer :
@@ -1686,6 +1755,7 @@ def clear_glyphs(self):
16861755 if self .rms is not None :
16871756 for line in self .rms :
16881757 line .remove ()
1758+ self .color_cycle .restore (line .get_color ())
16891759 self .rms = None
16901760 self ._renderer ._update ()
16911761
@@ -1732,10 +1802,12 @@ def plot_time_course(self, hemi, vertex_id, color, update=True):
17321802 except Exception :
17331803 mni = None
17341804 if mni is not None :
1735- mni = " MNI: " + ", " .join (f"{ m :5.1f} " for m in mni )
1805+ mni_str = ", " .join (f"{ m :5.1f} " for m in mni )
1806+ mni_suffix = " MNI: " + mni_str
17361807 else :
1737- mni = ""
1738- label = f"{ hemi_str } :{ str (vertex_id ).ljust (6 )} { mni } "
1808+ mni_str = None
1809+ mni_suffix = ""
1810+ label = f"{ hemi_str } :{ str (vertex_id ).ljust (6 )} { mni_suffix } "
17391811 act_data , smooth = self .act_data_smooth [hemi ]
17401812 if smooth is not None :
17411813 act_data = (smooth [[vertex_id ]] @ act_data )[0 ]
@@ -1745,11 +1817,14 @@ def plot_time_course(self, hemi, vertex_id, color, update=True):
17451817 time ,
17461818 act_data ,
17471819 label = label ,
1748- lw = 1.0 ,
1820+ lw = 1.8 ,
17491821 color = color ,
17501822 zorder = 4 ,
1751- update = update ,
1823+ update = False ,
17521824 )
1825+ self ._trace_meta [line ] = (hemi , vertex_id , mni_str )
1826+ if update :
1827+ self .mpl_canvas .update_plot ()
17531828 return line
17541829
17551830 @fill_doc
@@ -1770,7 +1845,9 @@ def plot_time_line(self, update=True):
17701845 x = current_time ,
17711846 label = "time" ,
17721847 color = self ._fg_color ,
1773- lw = 1 ,
1848+ lw = 1.5 ,
1849+ ls = "--" ,
1850+ alpha = 0.7 ,
17741851 update = update ,
17751852 )
17761853 self .time_line .set_xdata ([current_time ])
@@ -2133,6 +2210,36 @@ def add_data(
21332210 self ._all_data [key ][hemi ]["glyph_actor" ] = None
21342211 self ._all_data [key ][hemi ]["array" ] = array
21352212 self ._all_data [key ][hemi ]["vertices" ] = vertices
2213+ if (
2214+ stc is None
2215+ and hemi in ("lh" , "rh" )
2216+ and vertices is not None
2217+ and len (array ) == len (vertices )
2218+ ):
2219+ # Synthesize an stc from the raw arrays so that label-mode traces
2220+ # (which use stc.extract_label_time_course) also work when data
2221+ # is passed directly rather than plotted from an stc
2222+ from ...source_estimate import SourceEstimate , VectorSourceEstimate
2223+
2224+ stc_verts , stc_data = list (), list ()
2225+ for stc_hemi in ("lh" , "rh" ):
2226+ hemi_data = self ._all_data [key ].get (stc_hemi )
2227+ if not isinstance (hemi_data , dict ) or "array" not in hemi_data :
2228+ stc_verts .append (np .array ([], int ))
2229+ continue
2230+ stc_array = hemi_data ["array" ]
2231+ if stc_array .ndim == 1 :
2232+ stc_array = stc_array [:, np .newaxis ]
2233+ stc_verts .append (hemi_data ["vertices" ])
2234+ stc_data .append (stc_array )
2235+ if time is not None and len (time ) > 1 :
2236+ tmin , tstep = time [0 ], time [1 ] - time [0 ]
2237+ else :
2238+ tmin , tstep = 0.0 , 1.0
2239+ klass = VectorSourceEstimate if stc_data [0 ].ndim == 3 else SourceEstimate
2240+ self ._all_data [key ]["stc" ] = klass (
2241+ np .concatenate (stc_data ), stc_verts , tmin , tstep , subject = self ._subject
2242+ )
21362243 self ._all_data [key ]["alpha" ] = alpha
21372244 self ._all_data [key ]["colormap" ] = colormap
21382245 self ._all_data [key ]["center" ] = center
@@ -2502,7 +2609,9 @@ def add_label(
25022609 tc = stc .extract_label_time_course (
25032610 label , src = src , mode = self .label_extract_mode
25042611 )
2505- tc = tc [0 ] if tc .ndim == 2 else tc [0 , 0 , :]
2612+ tc = tc [0 ]
2613+ if tc .ndim == 2 : # vector data: show the norm across orientations
2614+ tc = np .linalg .norm (tc , axis = 0 )
25062615 color = next (self .color_cycle )
25072616 line = self .mpl_canvas .plot (
25082617 self ._data ["time" ], tc , label = label_name , color = color
@@ -3468,7 +3577,7 @@ def _on_annotation_hover(self, iren, event): # event == "MouseMoveEvent"
34683577
34693578 x , y = iren .GetEventPosition ()
34703579 picked_renderer = iren .FindPokedRenderer (x , y )
3471- vtk_picker = self ._renderer ._picker
3580+ vtk_picker = self ._renderer ._hover_picker
34723581 vtk_picker .Pick (x , y , 0 , picked_renderer )
34733582 cell_id = vtk_picker .GetCellId ()
34743583 # This returns a vtkPolyData we don't seem to have access to:
0 commit comments