spikeinterface
spikeinterface copied to clipboard
plot_traces and ipywidget backend can not handle channel_ids
If plot_traces is used with the ipywidget backend, and some channels_ids are given, there is a crash
I will fix it and also the channel order with ipywidgets is a bit weird
Did you fix this or is it still on the todo list?
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)
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!