Skip to content

Commit 198d783

Browse files
committed
Add LocationsWidgets and plot_locations
1 parent c5d3055 commit 198d783

3 files changed

Lines changed: 290 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
from warnings import warn
5+
6+
from .base import BaseWidget, to_attr
7+
from .utils import get_some_colors
8+
9+
from ..core.sortinganalyzer import SortingAnalyzer
10+
11+
12+
class LocationsWidget(BaseWidget):
13+
"""
14+
Plots spike locations as a function of time
15+
16+
Parameters
17+
----------
18+
sorting_analyzer : SortingAnalyzer
19+
The input sorting analyzer
20+
unit_ids : list or None, default: None
21+
List of unit ids
22+
segment_index : int or None, default: None
23+
The segment index (or None if mono-segment)
24+
max_spikes_per_unit : int or None, default: None
25+
Number of max spikes per unit to display. Use None for all spikes
26+
hide_unit_selector : bool, default: False
27+
If True the unit selector is not displayed
28+
(sortingview backend)
29+
plot_histogram : bool, default: False
30+
If True, an histogram of the locations is plotted on the right axis
31+
(matplotlib backend)
32+
bins : int or None, default: None
33+
If plot_histogram is True, the number of bins for the location histogram.
34+
If None this is automatically adjusted
35+
plot_legend : bool, default: True
36+
True includes legend in plot
37+
locations_axis : str, default: 'y'
38+
Which location axis to use when plotting locations.
39+
"""
40+
41+
def __init__(
42+
self,
43+
sorting_analyzer: SortingAnalyzer,
44+
unit_ids=None,
45+
unit_colors=None,
46+
segment_index=None,
47+
max_spikes_per_unit=None,
48+
hide_unit_selector=False,
49+
plot_histograms=False,
50+
bins=None,
51+
plot_legend=True,
52+
locations_axis="y",
53+
backend=None,
54+
**backend_kwargs,
55+
):
56+
57+
sorting_analyzer = self.ensure_sorting_analyzer(sorting_analyzer)
58+
59+
sorting = sorting_analyzer.sorting
60+
self.check_extensions(sorting_analyzer, "spike_locations")
61+
62+
locations = sorting_analyzer.get_extension("spike_locations").get_data(outputs="by_unit")
63+
64+
if unit_ids is None:
65+
unit_ids = sorting.unit_ids
66+
67+
if unit_colors is None:
68+
unit_colors = get_some_colors(sorting.unit_ids)
69+
70+
if sorting.get_num_segments() > 1:
71+
if segment_index is None:
72+
warn("More than one segment available! Using segment_index 0")
73+
segment_index = 0
74+
else:
75+
segment_index = 0
76+
locations_segment = locations[segment_index]
77+
total_duration = sorting_analyzer.get_num_samples(segment_index) / sorting_analyzer.sampling_frequency
78+
79+
spiketrains_segment = {}
80+
for i, unit_id in enumerate(sorting.unit_ids):
81+
times = sorting.get_unit_spike_train(unit_id, segment_index=segment_index)
82+
times = times / sorting.get_sampling_frequency()
83+
spiketrains_segment[unit_id] = times
84+
85+
all_spiketrains = spiketrains_segment
86+
all_locations = locations_segment
87+
if max_spikes_per_unit is not None:
88+
spiketrains_to_plot = dict()
89+
locations_to_plot = dict()
90+
for unit, st in all_spiketrains.items():
91+
locs = all_locations[unit][locations_axis]
92+
if len(st) > max_spikes_per_unit:
93+
random_idxs = np.random.choice(len(st), size=max_spikes_per_unit, replace=False)
94+
spiketrains_to_plot[unit] = st[random_idxs]
95+
locations_to_plot[unit] = locs[random_idxs]
96+
else:
97+
spiketrains_to_plot[unit] = st
98+
locations_to_plot[unit] = locs
99+
else:
100+
spiketrains_to_plot = all_spiketrains
101+
locations_to_plot = {
102+
unit_id: all_locations[unit_id][locations_axis] for unit_id in sorting_analyzer.unit_ids
103+
}
104+
105+
if plot_histograms and bins is None:
106+
bins = 100
107+
108+
plot_data = dict(
109+
sorting_analyzer=sorting_analyzer,
110+
locations=locations_to_plot,
111+
unit_ids=unit_ids,
112+
unit_colors=unit_colors,
113+
spiketrains=spiketrains_to_plot,
114+
total_duration=total_duration,
115+
plot_histograms=plot_histograms,
116+
bins=bins,
117+
hide_unit_selector=hide_unit_selector,
118+
plot_legend=plot_legend,
119+
)
120+
121+
BaseWidget.__init__(self, plot_data, backend=backend, **backend_kwargs)
122+
123+
def plot_matplotlib(self, data_plot, **backend_kwargs):
124+
import matplotlib.pyplot as plt
125+
from .utils_matplotlib import make_mpl_figure
126+
127+
dp = to_attr(data_plot)
128+
129+
if backend_kwargs["axes"] is not None:
130+
axes = backend_kwargs["axes"]
131+
if dp.plot_histograms:
132+
assert np.asarray(axes).size == 2
133+
else:
134+
assert np.asarray(axes).size == 1
135+
elif backend_kwargs["ax"] is not None:
136+
assert not dp.plot_histograms
137+
else:
138+
if dp.plot_histograms:
139+
backend_kwargs["num_axes"] = 2
140+
backend_kwargs["ncols"] = 2
141+
else:
142+
backend_kwargs["num_axes"] = None
143+
144+
self.figure, self.axes, self.ax = make_mpl_figure(**backend_kwargs)
145+
146+
scatter_ax = self.axes.flatten()[0]
147+
148+
for unit_id in dp.unit_ids:
149+
spiketrains = dp.spiketrains[unit_id]
150+
locs = dp.locations[unit_id]
151+
scatter_ax.scatter(spiketrains, locs, color=dp.unit_colors[unit_id], s=3, alpha=1, label=unit_id)
152+
153+
if dp.plot_histograms:
154+
if dp.bins is None:
155+
bins = int(len(spiketrains) / 30)
156+
else:
157+
bins = dp.bins
158+
ax_hist = self.axes.flatten()[1]
159+
count, bins = np.histogram(locs, bins=bins)
160+
ax_hist.plot(count, bins[:-1], color=dp.unit_colors[unit_id], alpha=0.8)
161+
162+
if dp.plot_histograms:
163+
ax_hist = self.axes.flatten()[1]
164+
ax_hist.set_ylim(scatter_ax.get_ylim())
165+
ax_hist.axis("off")
166+
# self.figure.tight_layout()
167+
168+
if dp.plot_legend:
169+
if hasattr(self, "legend") and self.legend is not None:
170+
self.legend.remove()
171+
self.legend = self.figure.legend(
172+
loc="upper center", bbox_to_anchor=(0.5, 1.0), ncol=5, fancybox=True, shadow=True
173+
)
174+
175+
scatter_ax.set_xlim(0, dp.total_duration)
176+
scatter_ax.set_xlabel("Times [s]")
177+
scatter_ax.set_ylabel(f"Location [um]")
178+
scatter_ax.spines["top"].set_visible(False)
179+
scatter_ax.spines["right"].set_visible(False)
180+
self.figure.subplots_adjust(bottom=0.1, top=0.9, left=0.1)
181+
182+
def plot_ipywidgets(self, data_plot, **backend_kwargs):
183+
import matplotlib.pyplot as plt
184+
185+
# import ipywidgets.widgets as widgets
186+
import ipywidgets.widgets as W
187+
from IPython.display import display
188+
from .utils_ipywidgets import check_ipywidget_backend, UnitSelector
189+
190+
check_ipywidget_backend()
191+
192+
self.next_data_plot = data_plot.copy()
193+
194+
cm = 1 / 2.54
195+
analyzer = data_plot["sorting_analyzer"]
196+
197+
width_cm = backend_kwargs["width_cm"]
198+
height_cm = backend_kwargs["height_cm"]
199+
200+
ratios = [0.15, 0.85]
201+
202+
with plt.ioff():
203+
output = W.Output()
204+
with output:
205+
self.figure = plt.figure(figsize=((ratios[1] * width_cm) * cm, height_cm * cm))
206+
plt.show()
207+
208+
self.unit_selector = UnitSelector(analyzer.unit_ids)
209+
self.unit_selector.value = list(analyzer.unit_ids)[:1]
210+
211+
self.checkbox_histograms = W.Checkbox(
212+
value=data_plot["plot_histograms"],
213+
description="hist",
214+
)
215+
216+
left_sidebar = W.VBox(
217+
children=[
218+
self.unit_selector,
219+
self.checkbox_histograms,
220+
],
221+
layout=W.Layout(align_items="center", width="100%", height="100%"),
222+
)
223+
224+
self.widget = W.AppLayout(
225+
center=self.figure.canvas,
226+
left_sidebar=left_sidebar,
227+
pane_widths=ratios + [0],
228+
)
229+
230+
# a first update
231+
self._full_update_plot()
232+
233+
self.unit_selector.observe(self._update_plot, names="value", type="change")
234+
self.checkbox_histograms.observe(self._full_update_plot, names="value", type="change")
235+
236+
if backend_kwargs["display"]:
237+
display(self.widget)
238+
239+
def _full_update_plot(self, change=None):
240+
self.figure.clear()
241+
data_plot = self.next_data_plot
242+
data_plot["unit_ids"] = self.unit_selector.value
243+
data_plot["plot_histograms"] = self.checkbox_histograms.value
244+
data_plot["plot_legend"] = False
245+
246+
backend_kwargs = dict(figure=self.figure, axes=None, ax=None)
247+
self.plot_matplotlib(data_plot, **backend_kwargs)
248+
self._update_plot()
249+
250+
def _update_plot(self, change=None):
251+
for ax in self.axes.flatten():
252+
ax.clear()
253+
254+
data_plot = self.next_data_plot
255+
data_plot["unit_ids"] = self.unit_selector.value
256+
data_plot["plot_histograms"] = self.checkbox_histograms.value
257+
data_plot["plot_legend"] = False
258+
259+
backend_kwargs = dict(figure=None, axes=self.axes, ax=None)
260+
self.plot_matplotlib(data_plot, **backend_kwargs)
261+
262+
self.figure.canvas.draw()
263+
self.figure.canvas.flush_events()

src/spikeinterface/widgets/tests/test_widgets.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,30 @@ def test_plot_spike_locations(self):
480480
self.sorting_analyzer_sparse, with_channel_ids=True, backend=backend, **self.backend_kwargs[backend]
481481
)
482482

483+
def test_plot_locations(self):
484+
possible_backends = list(sw.LocationsWidget.get_possible_backends())
485+
for backend in possible_backends:
486+
if backend not in self.skip_backends:
487+
sw.plot_locations(self.sorting_analyzer_dense, backend=backend, **self.backend_kwargs[backend])
488+
unit_ids = self.sorting_analyzer_dense.unit_ids[:4]
489+
sw.plot_locations(
490+
self.sorting_analyzer_dense, unit_ids=unit_ids, backend=backend, **self.backend_kwargs[backend]
491+
)
492+
sw.plot_locations(
493+
self.sorting_analyzer_dense,
494+
unit_ids=unit_ids,
495+
plot_histograms=True,
496+
backend=backend,
497+
**self.backend_kwargs[backend],
498+
)
499+
sw.plot_locations(
500+
self.sorting_analyzer_sparse,
501+
unit_ids=unit_ids,
502+
plot_histograms=True,
503+
backend=backend,
504+
**self.backend_kwargs[backend],
505+
)
506+
483507
def test_plot_similarity(self):
484508
possible_backends = list(sw.TemplateSimilarityWidget.get_possible_backends())
485509
for backend in possible_backends:

src/spikeinterface/widgets/widget_list.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .rasters import RasterWidget
2121
from .sorting_summary import SortingSummaryWidget
2222
from .spike_locations import SpikeLocationsWidget
23+
from .spike_locations_by_time import LocationsWidget
2324
from .spikes_on_traces import SpikesOnTracesWidget
2425
from .template_metrics import TemplateMetricsWidget
2526
from .template_similarity import TemplateSimilarityWidget
@@ -46,6 +47,7 @@
4647
CrossCorrelogramsWidget,
4748
DriftRasterMapWidget,
4849
ISIDistributionWidget,
50+
LocationsWidget,
4951
MotionWidget,
5052
MotionInfoWidget,
5153
MultiCompGlobalAgreementWidget,
@@ -121,6 +123,7 @@
121123
plot_crosscorrelograms = CrossCorrelogramsWidget
122124
plot_drift_raster_map = DriftRasterMapWidget
123125
plot_isi_distribution = ISIDistributionWidget
126+
plot_locations = LocationsWidget
124127
plot_motion = MotionWidget
125128
plot_motion_info = MotionInfoWidget
126129
plot_multicomparison_agreement = MultiCompGlobalAgreementWidget

0 commit comments

Comments
 (0)