spikeinterface icon indicating copy to clipboard operation
spikeinterface copied to clipboard

plot_traces and ipywidget backend can not handle channel_ids

Open yger opened this issue 2 years ago • 2 comments

If plot_traces is used with the ipywidget backend, and some channels_ids are given, there is a crash

yger avatar Oct 05 '23 09:10 yger

I will fix it and also the channel order with ipywidgets is a bit weird

samuelgarcia avatar Oct 05 '23 09:10 samuelgarcia

Did you fix this or is it still on the todo list?

zm711 avatar Jun 21 '24 14:06 zm711

I wanted to plot channels in reverse order and ran into a "weird" effect using channel_ids (version 0.102.0).

%matplotlib widget
from spikeinterface import extractors as se 
from spikeinterface import widgets as sw

# Load recording
recording_path = 'my_spike_glx_folder'
recording = se.read_spikeglx(recording_path, stream_name='imec0.ap', all_annotations=True)

sw.plot_traces(recording, channel_ids=['imec0.ap#AP3', 'imec0.ap#AP2', 'imec0.ap#AP1'],  backend='ipywidgets')

and it threw this error:

---------------------------------------------------------------------------
TraitError                                Traceback (most recent call last)
Cell In[39], line 4
      1 from spikeinterface import widgets as sw
      3 get_ipython().run_line_magic('matplotlib', 'widget')
----> 4 sw.plot_traces(recording, channel_ids=['imec0.ap#AP3', 'imec0.ap#AP1'], backend='ipywidgets')

File [~/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/traces.py:279](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/traces.py#line=278), in TracesWidget.__init__(self, recording, segment_index, channel_ids, order_channel_by_depth, time_range, mode, return_scaled, cmap, show_channel_ids, events, events_color, events_alpha, color_groups, color, clim, tile_size, seconds_per_row, scale, vspacing_factor, with_colorbar, add_legend, backend, **backend_kwargs)
    248 self.cb = None
    250 plot_data = dict(
    251     recordings=recordings,
    252     segment_index=segment_index,
   (...)    276     return_scaled=return_scaled,
    277 )
--> 279 BaseWidget.__init__(self, plot_data, backend=backend, **backend_kwargs)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/base.py:87](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/base.py#line=86), in BaseWidget.__init__(self, data_plot, backend, immediate_plot, **backend_kwargs)
     84 self.backend_kwargs = backend_kwargs_
     86 if immediate_plot:
---> 87     self.do_plot()

File [~/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/base.py:108](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/base.py#line=107), in BaseWidget.do_plot(self)
    106 def do_plot(self):
    107     func = getattr(self, f"plot_{self.backend}")
--> 108     func(self.data_plot, **self.backend_kwargs)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/traces.py:442](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/traces.py#line=441), in TracesWidget.plot_ipywidgets(self, data_plot, **backend_kwargs)
    440 self.scaler = ScaleWidget()
    441 self.channel_selector = ChannelSelector(self.rec0.channel_ids)
--> 442 self.channel_selector.value = list(data_plot["channel_ids"])
    444 left_sidebar_elements = [
    445     W.Label(value="layer"),
    446     self.layer_selector,
   (...)    450     self.colorbar,
    451 ]
    452 left_sidebar = W.VBox(
    453     children=left_sidebar_elements,
    454     align_items="center",
    455 )

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:716](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=715), in TraitType.__set__(self, obj, value)
    714 if self.read_only:
    715     raise TraitError('The "%s" trait is read-only.' % self.name)
--> 716 self.set(obj, value)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:3635](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=3634), in List.set(self, obj, value)
   3633     return super().set(obj, [value])  # type:ignore[list-item]
   3634 else:
-> 3635     return super().set(obj, value)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:706](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=705), in TraitType.set(self, obj, value)
    702     silent = False
    703 if silent is not True:
    704     # we explicitly compare silent to True just in case the equality
    705     # comparison above returns something other than True[/False](http://localhost:8888/False)
--> 706     obj._notify_trait(self.name, old_value, new_value)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:1513](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=1512), in HasTraits._notify_trait(self, name, old_value, new_value)
   1512 def _notify_trait(self, name: str, old_value: t.Any, new_value: t.Any) -> None:
-> 1513     self.notify_change(
   1514         Bunch(
   1515             name=name,
   1516             old=old_value,
   1517             new=new_value,
   1518             owner=self,
   1519             type="change",
   1520         )
   1521     )

File [~/miniconda3/envs/si/lib/python3.14/site-packages/ipywidgets/widgets/widget.py:701](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/ipywidgets/widgets/widget.py#line=700), in Widget.notify_change(self, change)
    698     if name in self.keys and self._should_send_property(name, getattr(self, name)):
    699         # Send new state to front-end
    700         self.send_state(key=name)
--> 701 super().notify_change(change)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:1525](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=1524), in HasTraits.notify_change(self, change)
   1523 def notify_change(self, change: Bunch) -> None:
   1524     """Notify observers of a change event"""
-> 1525     return self._notify_observers(change)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:1568](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=1567), in HasTraits._notify_observers(self, event)
   1565 elif isinstance(c, EventHandler) and c.name is not None:
   1566     c = getattr(self, c.name)
-> 1568 c(event)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/utils_ipywidgets.py:289](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/spikeinterface/widgets/utils_ipywidgets.py#line=288), in ChannelSelector.value_changed(self, change)
    287 i0 = self.channel_ids.index(channel_ids[0])
    288 i1 = self.channel_ids.index(channel_ids[-1]) + 1
--> 289 self.slider.value = (i0, i1)
    290 self.slider.observe(self.on_slider_changed, names=["value"], type="change")

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:716](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=715), in TraitType.__set__(self, obj, value)
    714 if self.read_only:
    715     raise TraitError('The "%s" trait is read-only.' % self.name)
--> 716 self.set(obj, value)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:690](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=689), in TraitType.set(self, obj, value)
    689 def set(self, obj: HasTraits, value: S) -> None:
--> 690     new_value = self._validate(obj, value)
    691     assert self.name is not None
    692     try:

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:724](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=723), in TraitType._validate(self, obj, value)
    722     value = self.validate(obj, value)
    723 if obj._cross_validation_lock is False:
--> 724     value = self._cross_validate(obj, value)
    725 return t.cast(G, value)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:730](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=729), in TraitType._cross_validate(self, obj, value)
    728 if self.name in obj._trait_validators:
    729     proposal = Bunch({"trait": self, "value": value, "owner": obj})
--> 730     value = obj._trait_validators[self.name](obj, proposal)
    731 elif hasattr(obj, "_%s_validate" % self.name):
    732     meth_name = "_%s_validate" % self.name

File [~/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py:1241](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/traitlets/traitlets.py#line=1240), in EventHandler.__call__(self, *args, **kwargs)
   1239 """Pass `*args` and `**kwargs` to the handler's function if it exists."""
   1240 if hasattr(self, "func"):
-> 1241     return self.func(*args, **kwargs)
   1242 else:
   1243     return self._init_call(*args, **kwargs)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/ipywidgets/widgets/widget_int.py:277](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/ipywidgets/widgets/widget_int.py#line=276), in _BoundedIntRange._validate_value(self, proposal)
    275 @validate('value')
    276 def _validate_value(self, proposal):
--> 277     lower, upper = super()._validate_value(proposal)
    278     lower, upper = min(lower, self.max), min(upper, self.max)
    279     lower, upper = max(lower, self.min), max(upper, self.min)

File [~/miniconda3/envs/si/lib/python3.14/site-packages/ipywidgets/widgets/widget_int.py:223](http://localhost:8888/home/ecobost/miniconda3/envs/si/lib/python3.14/site-packages/ipywidgets/widgets/widget_int.py#line=222), in _IntRange._validate_value(self, proposal)
    221 lower, upper = proposal['value']
    222 if upper < lower:
--> 223     raise TraitError('setting lower > upper')
    224 return lower, upper

TraitError: setting lower > upper

Other channel_id parameters that work/ don't work are:

Works: channel_ids=['imec0.ap#AP1', 'imec0.ap#AP2', 'imec0.ap#AP3'] channel_ids=['imec0.ap#AP1', 'imec0.ap#AP3'] channel_ids=['imec0.ap#AP2', 'imec0.ap#AP1'] # works and plots channel 2 before channel 1 channel_ids=['imec0.ap#AP3', 'imec0.ap#AP2']

Does not work: channel_ids=['imec0.ap#AP3', 'imec0.ap#AP2', 'imec0.ap#AP1'] channel_ids=['imec0.ap#AP3', 'imec0.ap#AP1']

Do you intend for people to be able to change the order of the channels arbitrarily with channel_ids? If not, it should throw a nicer error (and the example with only two channels should also error). And maybe there should be another option to reverse the channel order (seems like a pretty common use case)

ecobost avatar Dec 04 '25 02:12 ecobost

Hey @ecobost we definitely want users to be able to set any channel ids in any order they want. This is definitely a bug! Managed to reproduce the bug locally with generated data:

%matplotlib widget
import spikeinterface.full as si
recording, _ = si.generate_ground_truth_recording()
si.plot_traces(recording, channel_ids=['2', '1', '0'],  backend='ipywidgets')

Will look into it!

chrishalcrow avatar Dec 04 '25 11:12 chrishalcrow