Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f1a3365
wip
JoeZiminski Aug 28, 2026
e3c603e
update.
JoeZiminski Aug 28, 2026
434d58e
Merge branch 'main' into fix-raster-plot-segment
JoeZiminski Sep 16, 2026
b2b6112
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 16, 2026
22c6e16
Tidy up,tests.
JoeZiminski Sep 16, 2026
e572373
Tidy
JoeZiminski Sep 16, 2026
0d1745a
Fix linting.
JoeZiminski Sep 16, 2026
acf27b0
Small tidy up.
JoeZiminski Sep 16, 2026
1f31ce7
Fix sorting bug, roll out across all plots, remove durations.
JoeZiminski Sep 17, 2026
c9e8e77
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 17, 2026
3851db2
Update tests.
JoeZiminski Sep 17, 2026
9ead884
Add comment.
JoeZiminski Sep 17, 2026
98143d1
Remove unused var.
JoeZiminski Sep 17, 2026
539b7ed
Fix raster widgets.
JoeZiminski Sep 17, 2026
48ca1c9
Fix rasters.
JoeZiminski Sep 17, 2026
25de7b4
Convert to dict.
JoeZiminski Sep 17, 2026
5ae61c4
Util for conversion to boundaries.
JoeZiminski Sep 17, 2026
da4456c
Tidy.
JoeZiminski Sep 17, 2026
c000d5b
Linting.
JoeZiminski Sep 17, 2026
edf00d4
Remove stuff now in PR 4796.
JoeZiminski Sep 17, 2026
3e9d805
Fix test.
JoeZiminski Sep 17, 2026
033d3a3
Fix tests.
JoeZiminski Sep 17, 2026
244d985
Fix tests....again.
JoeZiminski Sep 17, 2026
6db46de
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 17, 2026
4c72f0e
Merge branch 'main' into fix-raster-plot-segment
JoeZiminski Oct 14, 2026
a416cfe
Dynamically use tick labels.
JoeZiminski Oct 14, 2026
7939950
Remove using get segment times on sorting for now.
JoeZiminski Oct 14, 2026
7d102e2
Use recording times for start/stop times if htey exist in get_segment…
JoeZiminski Oct 14, 2026
14acc79
Linting.
JoeZiminski Oct 14, 2026
5e4bf2f
Remove playing.
JoeZiminski Oct 14, 2026
c17bbbb
Minor tidy ups and refactorings.
JoeZiminski Oct 14, 2026
6336bc2
More dynamic ticks.
JoeZiminski Oct 14, 2026
266d837
Think foudn a happy medium with the ticks.
JoeZiminski Oct 14, 2026
a34b786
rename compute to estimate.
JoeZiminski Oct 16, 2026
e063ecf
Remove some locator formatting as default works pretty well.
JoeZiminski Oct 16, 2026
9a9d3e2
Small doc tidying up.
JoeZiminski Oct 16, 2026
3851e2d
ensure all methods return inclusive stop times.
JoeZiminski Oct 16, 2026
eddf608
Linting.
JoeZiminski Oct 16, 2026
02a7b1d
Better handle empty segments.
JoeZiminski Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/spikeinterface/core/sorting_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def random_spikes_selection(
if maximum_rate is None:
raise ValueError(f"maximum_rate must be defined")

t_duration = np.sum(get_segment_durations(sorting))
t_duration = np.sum(get_segment_durations(sorting)[0])
rng_size = min(int(t_duration * maximum_rate), max_spikes_per_unit, all_unit_indices.size)
selected_unit_indices = rng.choice(all_unit_indices, size=rng_size, replace=False, shuffle=False)

Expand Down
15 changes: 10 additions & 5 deletions src/spikeinterface/widgets/amplitudes.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,15 @@ def __init__(
if plot_histograms and bins is None:
bins = 100

# Calculate durations for all segments for x-axis limits
durations = get_segment_durations(sorting, segment_indices)
_, segment_start_stop_times = get_segment_durations(sorting, segment_indices)

# Build the plot data with the full dict of dicts structure
plot_data = dict(
unit_colors=unit_colors,
plot_histograms=plot_histograms,
bins=bins,
durations=durations,
segment_start_stop_times=segment_start_stop_times,
sampling_frequency=sorting.get_sampling_frequency(),
unit_ids=unit_ids,
hide_unit_selector=hide_unit_selector,
plot_legend=plot_legend,
Expand Down Expand Up @@ -180,9 +180,14 @@ def plot_figpack(self, data_plot, **backend_kwargs):
for u in unit_ids
]

segment_times = dp.segment_start_stop_times.values()

start_time_sec = min(start for start, _ in segment_times)
end_time_sec = max(stop for _, stop in segment_times)

self.view = vv_views.SpikeAmplitudes(
start_time_sec=0,
end_time_sec=np.sum(dp.durations),
start_time_sec=start_time_sec,
end_time_sec=end_time_sec,
plots=sa_items,
# hide_unit_selector=dp.hide_unit_selector,
)
Expand Down
25 changes: 12 additions & 13 deletions src/spikeinterface/widgets/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from spikeinterface.core import BaseRecording, SortingAnalyzer
from .rasters import BaseRasterWidget
from .utils import estimate_segment_durations_from_spike_vector, get_segment_start_stop_times
from spikeinterface.core.motion import Motion


Expand Down Expand Up @@ -155,7 +156,6 @@ def __init__(
color: str = "Gray",
clim: tuple[float, float] | None = None,
alpha: float = 1,
segment_index: int | list[int] | None = None,

@JoeZiminski JoeZiminski Sep 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused in main, but I guess this is public facing and will need a deprecation warning? (although maybe not if its never used anyway)

backend: str | None = None,
**backend_kwargs,
):
Expand Down Expand Up @@ -229,7 +229,10 @@ def __init__(
segment_locations = filtered_locations[segment_mask]

# Convert peak times to seconds
spike_times = segment_peaks["sample_index"] / sampling_frequency
if recording is None:
spike_times = segment_peaks["sample_index"] / sampling_frequency
else:
spike_times = recording.sample_index_to_time(segment_peaks["sample_index"], segment_index=seg_idx)

# Store in dict of dicts format (using 0 as the "unit" id)
spike_train_data[seg_idx] = {0: spike_times}
Expand Down Expand Up @@ -257,19 +260,14 @@ def __init__(
else:
color_kwargs = dict(color=color, c=None, alpha=alpha)

# Calculate segment durations for x-axis limits
# Calculate segment start/stop times for x-axis limits
if recording is not None:
durations = [recording.get_duration(seg_idx) for seg_idx in segment_indices]
segment_start_stop_times = get_segment_start_stop_times(recording, segment_indices)
else:
# Find boundaries between segments using searchsorted
segment_boundaries = [
np.searchsorted(filtered_peaks["segment_index"], [seg_idx, seg_idx + 1]) for seg_idx in segment_indices
]

# Calculate durations from max sample in each segment
durations = [
(filtered_peaks["sample_index"][end - 1] + 1) / sampling_frequency for (_, end) in segment_boundaries
]
_, segment_start_stop_times = estimate_segment_durations_from_spike_vector(
filtered_peaks, segment_indices, sampling_frequency
)

plot_data = dict(
spike_train_data=spike_train_data,
Expand All @@ -280,7 +278,8 @@ def __init__(
scatter_decimate=scatter_decimate,
title="Peak depth",
y_label="Depth [um]",
durations=durations,
segment_start_stop_times=segment_start_stop_times,
sampling_frequency=sampling_frequency,
)

BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs)
Expand Down
103 changes: 71 additions & 32 deletions src/spikeinterface/widgets/rasters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

from spikeinterface.core import SortingAnalyzer, BaseSorting
from .base import BaseWidget, to_attr, default_backend_kwargs
from .utils import get_some_colors, validate_segment_indices, get_segment_durations
from .utils import (
get_some_colors,
validate_segment_indices,
get_segment_durations,
)


class BaseRasterWidget(BaseWidget):
Expand All @@ -25,8 +29,10 @@ class BaseRasterWidget(BaseWidget):
segment_indices : list | None, default: None
For multi-segment data, specifies which segment(s) to plot. If None, uses all available segments.
For single-segment data, this parameter is ignored.
durations : list | None, default: None
segment_start_stop_times : dict | None, default: None
List of durations per segment of spike_train_data in seconds.
sampling_frequency : float | None, default: None
Sampling frequency in Hz, required to space multiple segments.
plot_histograms : bool, default: False
Plot histogram of y-axis data in another subplot
bins : int | None, default: None
Expand Down Expand Up @@ -67,7 +73,7 @@ def __init__(
sort_by_depth: bool = False,
unit_ids: list | None = None,
segment_indices: list | None = None,
durations: list | None = None,
segment_start_stop_times: dict[int, tuple[float, float]] | None = None,
plot_histograms: bool = False,
bins: int | None = None,
scatter_decimate: int = 1,
Expand All @@ -82,9 +88,9 @@ def __init__(
hide_unit_selector: bool = True,
segment_boundary_kwargs: dict | None = None,
backend: str | None = None,
sampling_frequency: float | None = None,
**backend_kwargs,
):

# Set default segment boundary kwargs if not provided
if segment_boundary_kwargs is None:
segment_boundary_kwargs = {"color": "gray", "linestyle": "--", "alpha": 0.7}
Expand Down Expand Up @@ -113,16 +119,27 @@ def __init__(
all_units.update(spike_train_data[seg_idx].keys())
unit_ids = list(all_units)

# Calculate cumulative durations for segment boundaries
segment_boundaries = np.cumsum(durations)
cumulative_durations = np.concatenate([[0], segment_boundaries])
segment_time_offsets = None
# Drop segment times we don't want to use
if segment_start_stop_times is not None:
segment_start_stop_times = {seg: segment_start_stop_times[seg] for seg in segments_to_use}

if len(segment_start_stop_times) > 1:
if sampling_frequency is None:
raise ValueError("sampling_frequency is required when plotting multiple segments")
# Align the segment times such that consecutive segments appear directly next to each other
# on the plot, rather than at their true times (if true times are used, large inter-segment
# gaps can appear on the plot)
starts, stops = np.array(list(segment_start_stop_times.values())).T
ts = 1 / sampling_frequency
segment_time_offsets = np.r_[0, np.cumsum(starts[1:] - (stops[:-1] + ts))]

# Concatenate data across segments with proper time offsets
concatenated_spike_trains = {unit_id: np.array([]) for unit_id in unit_ids}
concatenated_y_axis = {unit_id: np.array([]) for unit_id in unit_ids}

for offset, spike_train_segment, y_axis_segment in zip(
cumulative_durations,
for segment_index, spike_train_segment, y_axis_segment in zip(
range(len(segments_to_use)),
[spike_train_data[idx] for idx in segments_to_use],
[y_axis_data[idx] for idx in segments_to_use],
):
Expand All @@ -134,13 +151,11 @@ def __init__(
# Get y-axis values for this unit
y_values = y_axis_segment[unit_id]

# Apply offset to spike times
adjusted_times = spike_times + offset
if segment_time_offsets is not None:
spike_times = spike_times - segment_time_offsets[segment_index]

# Add to concatenated data
concatenated_spike_trains[unit_id] = np.concatenate(
[concatenated_spike_trains[unit_id], adjusted_times]
)
concatenated_spike_trains[unit_id] = np.concatenate([concatenated_spike_trains[unit_id], spike_times])
concatenated_y_axis[unit_id] = np.concatenate([concatenated_y_axis[unit_id], y_values])

plot_data = dict(
Expand All @@ -157,12 +172,12 @@ def __init__(
unit_colors=unit_colors,
y_label=y_label,
title=title,
durations=durations,
segment_start_stop_times=segment_start_stop_times,
segment_time_offsets=segment_time_offsets,
plot_legend=plot_legend,
bins=bins,
y_ticks=y_ticks,
hide_unit_selector=hide_unit_selector,
segment_boundaries=segment_boundaries,
segment_boundary_kwargs=segment_boundary_kwargs,
)

Expand Down Expand Up @@ -233,11 +248,6 @@ def plot_matplotlib(self, data_plot, **backend_kwargs):
count, bins = np.histogram(unit_y_data, bins=bins)
ax_hist.plot(count, bins[:-1], color=unit_colors[unit_id], alpha=0.8)

# Add segment boundary lines if provided
if getattr(dp, "segment_boundaries", None) is not None:
for boundary in dp.segment_boundaries:
scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs)

if dp.plot_histograms:
ax_hist = self.axes.flatten()[1]
ax_hist.set_ylim(scatter_ax.get_ylim())
Expand All @@ -253,9 +263,23 @@ def plot_matplotlib(self, data_plot, **backend_kwargs):
if dp.y_lim is not None:
scatter_ax.set_ylim(*dp.y_lim)
x_lim = dp.x_lim
if x_lim is None:
x_lim = [0, np.sum(dp.durations)]
scatter_ax.set_xlim(x_lim)

# Add segment boundary lines if provided and handle x limits
if dp.segment_start_stop_times is not None:

segment_times = np.array(list(dp.segment_start_stop_times.values()))
if len(segment_times) > 1:

segment_times = segment_times - dp.segment_time_offsets[:, None]
self._set_segment_time_ticks(scatter_ax, segment_times, dp.segment_time_offsets)

for boundary in segment_times[1:, 0]:
scatter_ax.axvline(boundary, **dp.segment_boundary_kwargs)

if x_lim is None:
x_lim = (segment_times[0, 0], segment_times[-1, 1])
if x_lim is not None:
scatter_ax.set_xlim(x_lim)

if dp.sort_by_depth and dp.depth_dict is not None:
scatter_ax.set_yticks(ticks=list(range(len(dp.depth_dict))), labels=list(dp.depth_dict.keys()))
Expand Down Expand Up @@ -381,6 +405,24 @@ def _update_plot(self, change=None):
self.figure.canvas.draw()
self.figure.canvas.flush_events()

@staticmethod
def _set_segment_time_ticks(ax, segment_times, segment_offsets):
"""Label concatenated segments with original times, using default tick placement.

``segment_times`` contains displayed bounds; ``segment_offsets`` restores
each segment's original timestamps after gaps have been removed.
"""
from matplotlib.ticker import ScalarFormatter

time_formatter = ScalarFormatter()
time_formatter.set_axis(ax.xaxis)

def format_original_time(x, pos):
segment = np.searchsorted(segment_times[1:, 0], x, side="right")
return time_formatter.format_data_short(x + segment_offsets[segment])

ax.xaxis.set_major_formatter(format_original_time)


class RasterWidget(BaseRasterWidget):
"""
Expand Down Expand Up @@ -443,20 +485,16 @@ def __init__(
# Create a lookup dictionary for unit indices
unit_indices_map = {unit_id: i for i, unit_id in enumerate(unit_ids)}

# Estimate segment duration from max spike time in each segment
durations = get_segment_durations(sorting, segment_indices)

# Extract spike data for all segments and units at once
spike_train_data = {seg_idx: {} for seg_idx in segment_indices}
y_axis_data = {seg_idx: {} for seg_idx in segment_indices}

_, segment_start_stop_times = get_segment_durations(sorting, segment_indices)

for seg_idx in segment_indices:
for unit_id in unit_ids:
# Get spikes for this segment and unit
spike_times = (
sorting.get_unit_spike_train(unit_id=unit_id, segment_index=seg_idx) / sorting.sampling_frequency
)

spike_times = sorting.get_unit_spike_train_in_seconds(unit_id=unit_id, segment_index=seg_idx)
# Store data
spike_train_data[seg_idx][unit_id] = spike_times
y_axis_data[seg_idx][unit_id] = unit_indices_map[unit_id] * np.ones(len(spike_times))
Expand Down Expand Up @@ -486,7 +524,8 @@ def __init__(
unit_colors=unit_colors,
plot_histograms=None,
y_ticks=y_ticks,
durations=durations,
segment_start_stop_times=segment_start_stop_times,
sampling_frequency=sorting.get_sampling_frequency(),
)

BaseRasterWidget.__init__(self, **plot_data, backend=backend, **backend_kwargs)
15 changes: 15 additions & 0 deletions src/spikeinterface/widgets/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,11 @@ def test_plot_rasters(self):
if backend not in self.skip_backends:
sw.plot_rasters(self.sorting)

shifted_sorting = self.sorting.clone()
shifted_sorting.shift_times(5.0)
widget = sw.plot_rasters(shifted_sorting, backend="matplotlib")
assert widget.ax.get_xlim()[0] >= 5.0

def test_plot_unit_probe_map(self):
possible_backends = list(sw.UnitProbeMapWidget.get_possible_backends())
for backend in possible_backends:
Expand Down Expand Up @@ -691,6 +696,16 @@ def test_drift_raster_map(self):
sw.plot_drift_raster_map(
peaks=peaks, peak_locations=peak_locations, recording=recording, color_amplitude=True
)
if backend == "matplotlib":
shifted_recording = recording.clone()
shifted_recording.shift_times(5.0)
widget = sw.plot_drift_raster_map(
peaks=peaks, peak_locations=peak_locations, recording=shifted_recording
)
np.testing.assert_allclose(
widget.ax.get_xlim(),
[shifted_recording.get_start_time(), shifted_recording.get_end_time()],
)
# without recording
sw.plot_drift_raster_map(
peaks=peaks,
Expand Down
Loading
Loading