# ~/analyseur/cbgtc/visual/markerplot.py
#
# Documentation by Lungsi 29 Oct 2025
#
# This contains function for SpikingStats
#
"""
===============
Marker Plotting
===============
+------------------------------+-----------------------------------------------------------------------------------------------------+
| Functions | Purpose |
+==============================+=====================================================================================================+
| :func:`plot_raster` | plots Coefficient of Variations of all the neurons in a population |
+------------------------------+-----------------------------------------------------------------------------------------------------+
| :func:`plot_ratechange` | draws the Coefficient of Variations of all the neurons into a given `matplotlib.pyplot.axis` |
+------------------------------+-----------------------------------------------------------------------------------------------------+
| :func:`plot_ratechange_in_ax`| plots Local Coefficient of Variations of all the neurons in a population |
+------------------------------+-----------------------------------------------------------------------------------------------------+
--------------------------
Raster Plot of Spike Times
--------------------------
1. Pre-requisites
=================
1.1. Import Modules
-------------------
::
from analyseur.cbgtc.loader import LoadSpikeTimes
from analyseur.cbgtc.visual.markerplot import plot_raster
1.2. Load file and get spike times
----------------------------------
::
loadST = LoadSpikeTimes("spikes_GPi.csv")
spiketimes_superset = loadST.get_spiketimes_superset()
2. Cases
========
2.1. Raster for all the neurons
-------------------------------
::
plot_raster(spiketimes_superset)
2.2. Raster for first 50 neurons
--------------------------------
::
plot_raster(spiketimes_superset, neurons=range(50))
2.3. Raster for second 50 neurons
---------------------------------
::
plot_raster(spiketimes_superset, neurons=range(50, 100))
2.4. Create the plot for customization
``````````````````````````````````````
This is for power users who for instance want to insert the raster plot in their
collage of subplots.
::
import matplotlib.pyplot as plt
from analyseur.cbgtc.visual.markerplot import plot_raster_in_ax
fig, (ax1, ax2) = plt.subplots(1, 2)
fig.suptitle('Horizontally stacked subplots')
ax1 = plot_raster_in_ax(ax1, spiketimes_superset)
ax2 = plot_raster_in_ax(ax2, spiketimes_superset)
plt.show()
NOTE: This example shows :func:`plot_raster_in_ax` in default setting but this function works like
:func:`plot_raster` therefore all the cases 2.1, 2.2 and 2.3 are applicable for :func:`plot_raster_in_ax`.
------------------------
Plot Rate Change Scatter
------------------------
1. Pre-requisites
=================
1.1. Import Modules
-------------------
::
from analyseur.cbgtc.loader import LoadSpikeTimes
from analyseur.cbgtc.visual.markerplot import plot_ratechange
1.2. Load file and get spike times
----------------------------------
::
loadST = LoadSpikeTimes("spikes_GPi.csv")
spiketimes_superset = loadST.get_spiketimes_superset()
2. Cases
========
2.1. Plot Rate Change Scatter for all the neurons
-------------------------------------------------
::
plot_ratechange(spiketimes_superset)
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
import numbers
import matplotlib.pyplot as plt
import numpy as np
from analyseur.cbgtc.curate import get_desired_spiketimes_subset
from analyseur.cbgtc.parameters import SignalAnalysisParams, SimulationParams
__siganal = SignalAnalysisParams()
__simparams = SimulationParams()
##########################################################################
# Raster Plot
##########################################################################
def _get_line_colors(colors=False, no_neurons=None):
if colors:
return [f'C{i}' for i in range(no_neurons)] # set different colors for each set of positions
else:
return "black"
def __plot_raster_in_ax(ax, spiketimes_superset, window=None, colors=False, neurons=None, nucleus=None,):
"""
Draws the Rasterplot (`matplotlib.pyplot.eventplot <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.eventplot.html>`_)
on the given
`matplotlib.pyplot.axis <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axis.html>`_
:param ax: object `matplotlib.pyplot.axis``
:param spiketimes_superset: Dictionary returned using :class:`~analyseur/cbgtc/loader.LoadSpikeTimes`
OPTIONAL parameters
:param window: Tuple in the form `(start_time, end_time)`; `(0, 10)` [default]
:param colors: `False` [default] or True
:param neurons: `"all"` [default] or `range(a, b)` or list of neuron ids like `[2, 3, 6, 7]
:param nucleus: string; name of the nucleus
:return: object `ax` with Raster plotting done into it
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
# ============== DEFAULT Parameters ==============
if neurons is None:
neurons = "all"
elif isinstance(neurons, numbers.Number):
neuron_ids = dict(list(spiketimes_superset.items())[:neurons]).keys()
neurons = [int(item[1:]) for item in neuron_ids]
if window is None:
window = __siganal.window
[desired_spiketimes_subset, yticks] = get_desired_spiketimes_subset(spiketimes_superset,
window=window,
neurons=neurons)
n_neurons = len(desired_spiketimes_subset)
linecolors = _get_line_colors(colors=colors, no_neurons=n_neurons)
# ====== PLOT PARAMETERS ======
n_yticks = 20
# linelengths = 0.25 # default: 1
# linewidths = 3.0 # default: 1.5
spacing = 0.1 # 0.25
linelengths = spacing * 0.7 # 60-80% of spacing to avoid overlapping
linewidths = 10 * (spacing / 0.8)
# ytick_trigger = 50
if n_neurons > 50:
ytick_interval = int(n_neurons / n_yticks)
else:
ytick_interval = 1
# lineoffsets = 0.5 # default: 1
lineoffsets = np.arange(1, n_neurons + 1) * spacing # 0.5 spacing = below
# lineoffsets = np.arange(n_neurons) * 0.8 + 0.5 # minimal spacing between neurons
# Plot
ax.eventplot(desired_spiketimes_subset, colors=linecolors,
linelengths=linelengths, linewidths=linewidths,
lineoffsets=lineoffsets,orientation="horizontal", alpha=None)
# if n_neurons > ytick_trigger:
# # plt.yticks([])
# plt.yticks(lineoffsets[::ytick_interval], yticks[::ytick_interval])
# else:
# plt.yticks(lineoffsets, yticks)
ax.set_yticks(lineoffsets[::ytick_interval], yticks[::ytick_interval])
ax.set_ylabel("neurons")
ax.set_xlabel("Time (s)")
ax.set_ylim(0, lineoffsets[-1] + 0.5) # visibile y-range to eliminate white space
nucname = "" if nucleus is None else " in "+nucleus
allno = str(n_neurons)
if neurons=="all":
ax.set_title("Raster of all (" + allno + ") the neurons" + nucname)
else:
ax.set_title("Raster of " + str(neurons[0]) + " to " + str(neurons[-1]) + " neurons" + nucname)
return ax
[docs]
def plot_raster_in_ax(ax, spiketimes_superset, window=None, colors=False, neurons=None, nucleus=None, alpha=True):
"""
.. code-block:: text
Raster representation:
neuron ↑
n3 | . . . . .
n2 | . . .
n1 | . . . .
└──────────── time →
Draws the Rasterplot (`matplotlib.pyplot.eventplot <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.eventplot.html>`_)
on the given
`matplotlib.pyplot.axis <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axis.html>`_
:param ax: object `matplotlib.pyplot.axis``
:param spiketimes_superset: Dictionary returned using :class:`~analyseur/cbgtc/loader.LoadSpikeTimes`
OPTIONAL parameters
:param window: Tuple in the form `(start_time, end_time)`; `(0, 10)` [default]
:param colors: `False` [default] or `True`
:param neurons: `"all"` [default] or `range(a, b)` or list of neuron ids like `[2, 3, 6, 7]
:param nucleus: string; name of the nucleus
:param alpha: `True` [default]
:return: object `ax` with Raster plotting done into it
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
# ============== DEFAULT Parameters ==============
if neurons is None:
neurons = "all"
elif isinstance(neurons, numbers.Number):
neuron_ids = dict(list(spiketimes_superset.items())[:neurons]).keys()
neurons = [int(item[1:]) for item in neuron_ids]
if window is None:
window = __siganal.window
[desired_spiketimes_subset, yticks] = get_desired_spiketimes_subset(spiketimes_superset,
window=window,
neurons=neurons)
n_neurons = len(desired_spiketimes_subset)
linecolors = _get_line_colors(colors=colors, no_neurons=n_neurons)
# ====== PLOT PARAMETERS ======
n_yticks = 20
# linelengths = 0.25 # default: 1
# linewidths = 3.0 # default: 1.5
spacing = 0.1 # 0.25
linelengths = spacing * 0.7 # 60-80% of spacing to avoid overlapping
linewidths = 10 * (spacing / 0.8)
# ytick_trigger = 50
if n_neurons > 50:
ytick_interval = int(n_neurons / n_yticks)
else:
ytick_interval = 1
# lineoffsets = 0.5 # default: 1
lineoffsets = np.arange(1, n_neurons + 1) * spacing # 0.5 spacing = below
# lineoffsets = np.arange(n_neurons) * 0.8 + 0.5 # minimal spacing between neurons
# Plot
x = np.concatenate(desired_spiketimes_subset)
y = np.concatenate([
np.full(len(spikes), lineoffsets[i])
for i, spikes in enumerate(desired_spiketimes_subset)
])
n_spikes = max(len(x), 1)
marker_size = np.clip(20000 / n_spikes, 0.5, 3)
if alpha:
density = n_spikes / (n_neurons * (window[1] - window[0]))
alpha = np.clip(1 / (1 + density), 0.05, 0.7)
else:
alpha = None
ax.scatter(
x,
y,
s=marker_size,
c="black",
marker=".",
alpha=alpha,
linewidths=0
)
ax.set_yticks(lineoffsets[::ytick_interval], yticks[::ytick_interval])
ax.set_ylabel("neurons")
ax.set_xlabel("Time (s)")
ax.set_ylim(0, lineoffsets[-1] + 0.5) # visibile y-range to eliminate white space
nucname = "" if nucleus is None else " in "+nucleus
allno = str(n_neurons)
if neurons=="all":
ax.set_title("Raster of all (" + allno + ") the neurons" + nucname)
else:
ax.set_title("Raster of " + str(neurons[0]) + " to " + str(neurons[-1]) + " neurons" + nucname)
return ax
[docs]
def plot_raster(spiketimes_superset, colors=False, neurons=None, nucleus=None, alpha=True):
"""
Visualize Raster plot for the given neuron population using :py:func:`plot_raster_in_ax`.
:param spiketimes_superset: Dictionary returned using :class:`~analyseur.cbgtc.loader.LoadSpikeTimes`
OPTIONAL parameters
:param colors: `False` [default] or `True`
:param neurons: `"all"` [default] or `range(a, b)` or list of neuron ids like `[2, 3, 6, 7]
:param nucleus: string; name of the nucleus
:param alpha: `True` [default]
:return: object `ax` with Raster plotting done into it
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
fig, ax = plt.subplots(figsize=(18, 12))
ax = plot_raster_in_ax(ax, spiketimes_superset, colors=colors,
neurons=neurons, nucleus=nucleus, alpha=alpha)
plt.show()
return fig, ax
##########################################################################
# Rate Change SCATTER
##########################################################################
def __plot_ratechange_in_ax(ax, spiketimes_superset, stimulus_onset=None,
window=None, neurons=None, nucleus=None, mode=None):
"""
Draws the Population Rate Change Scatter on the given
`matplotlib.pyplot.axis <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axis.html>`_
:param ax: object `matplotlib.pyplot.axis``
:param spiketimes_superset: Dictionary returned using :meth:`analyseur.cbgtc.stats.isi.InterSpikeInterval.compute`
OPTIONAL parameters
:param stimulus_onset: float; 0 [default]
:param window: 2-tuple; (0, 10) [default]
:param neurons: "all" [default] or list: range(a, b) or [1, 4, 5, 9]
:param nucleus: string; name of the nucleus
:param mode: "portrait" or None/landscape [default]
:return: object `ax` with Rate Distribution plotting done into it
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
# ============== DEFAULT Parameters ==============
if neurons is None:
neurons = "all"
elif isinstance(neurons, numbers.Number):
neurons = range(neurons)
if window is None:
window = __siganal.window
if stimulus_onset is None:
stimulus_onset = 0
[desired_spiketimes_subset, _] = get_desired_spiketimes_subset(spiketimes_superset, neurons=neurons)
n_neurons = len(desired_spiketimes_subset)
match mode:
case "portrait":
orient = "horizontal"
case _:
orient = "landscape"
get_axis = lambda orient: "x" if orient == "horizontal" else "y"
# Compute Rate Change
baseline_rates = []
response_rates = []
for indiv_spiketimes in desired_spiketimes_subset:
indiv_spiketimes = np.array(indiv_spiketimes)
baseline_spikes = indiv_spiketimes[(indiv_spiketimes >= window[0]) & (indiv_spiketimes < stimulus_onset)]
response_spikes = indiv_spiketimes[indiv_spiketimes >= stimulus_onset]
baseline_rate = len(baseline_spikes) / ((stimulus_onset - window[0]) + 1e-8)
response_rate = len(response_spikes) / ((window[1] - stimulus_onset) + 1e-8)
baseline_rates.append(baseline_rate)
response_rates.append(response_rate)
# Plot
if orient=="horizontal":
ax.scatter(response_rates, baseline_rates, alpha=0.6, color="orange")
ax.plot([0, max(baseline_rates)], [0, max(baseline_rates)],
"k--", alpha=0.5, label="No Change")
ax.set_ylabel("Baseline Rate (Hz)")
ax.set_xlabel("Response Rate (Hz)")
else:
ax.scatter(baseline_rates, response_rates, alpha=0.6, color="orange")
ax.plot([0, max(baseline_rates)], [0, max(baseline_rates)],
"k--", alpha=0.5, label="No Change")
ax.set_ylabel("Response Rate (Hz)")
ax.set_xlabel("Baseline Rate (Hz)")
ax.grid(True, alpha=0.3, axis=get_axis(orient))
nucname = "" if nucleus is None else " in " + nucleus
ax.set_title("Rate Change: Baseline vs. Response of " + str(n_neurons) + " neurons" + nucname)
return ax
[docs]
def plot_ratechange_in_ax(ax, spiketimes_superset, stimulus_onset=None, window=None,
neurons=None, nucleus=None, mode=None, alpha=True):
"""
.. code-block:: text
Each point represents one neuron.
y-axis : response firing rate (Hz)
x-axis : baseline firing rate (Hz)
Points above the diagonal → increased firing after stimulus
Points below the diagonal → decreased firing after stimulus
response ↑
|
↑ | *
increase | * *
| *
| *
----------+----------------------→ baseline
| *
decrease | *
↓ | *
|
+----------------------
dashed line = no change
(response rate = baseline rate)
Draws the Population Rate Change Scatter on the given
`matplotlib.pyplot.axis <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axis.html>`_
:param ax: object `matplotlib.pyplot.axis``
:param spiketimes_superset: Dictionary returned using :meth:`analyseur.cbgtc.stats.isi.InterSpikeInterval.compute`
OPTIONAL parameters
:param stimulus_onset: float; 0 [default]
:param window: 2-tuple; (0, 10) [default]
:param neurons: "all" [default] or list: range(a, b) or [1, 4, 5, 9]
:param nucleus: string; name of the nucleus
:param mode: "portrait" or None/landscape [default]
:param alpha: `True` [default]
:return: object `ax` with Rate Distribution plotting done into it
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
# ============== DEFAULT Parameters ==============
if neurons is None:
neurons = "all"
elif isinstance(neurons, numbers.Number):
neurons = range(neurons)
if window is None:
window = __siganal.window
if stimulus_onset is None:
stimulus_onset = 0
[desired_spiketimes_subset, _] = get_desired_spiketimes_subset(spiketimes_superset, neurons=neurons)
n_neurons = len(desired_spiketimes_subset)
match mode:
case "portrait":
orient = "horizontal"
case _:
orient = "landscape"
get_axis = lambda orient: "x" if orient == "horizontal" else "y"
# Compute Rate Change
baseline_rates = np.empty(n_neurons)
response_rates = np.empty(n_neurons)
for i, spikes in enumerate(desired_spiketimes_subset):
spikes = np.asarray(spikes)
baseline_rates[i] = np.sum(
(spikes >= window[0]) & (spikes < stimulus_onset)
) / ((stimulus_onset - window[0]) + 1e-8)
response_rates[i] = np.sum(
spikes >= stimulus_onset
) / ((window[1] - stimulus_onset) + 1e-8)
# Plot
n_points = max(n_neurons, 1)
marker_size = np.clip(2000 / n_points, 2, 15)
if alpha:
alpha = np.clip(5000 / n_points, 0.2, 0.8)
else:
alpha = None
max_rate = max(np.max(baseline_rates), np.max(response_rates))
delta_rate = response_rates - baseline_rates
if orient=="horizontal":
ax.scatter(response_rates, baseline_rates, c=delta_rate,
s=marker_size, alpha=alpha,
cmap="coolwarm", edgecolors="none",)
cbar = plt.colorbar(ax.collections[0], ax=ax)
cbar.set_label("Δ firing rate (Hz)")
ax.plot([0, max_rate], [0, max_rate],
"k--", alpha=0.5, label="No Change")
ax.set_ylabel("Baseline Rate (Hz)")
ax.set_xlabel("Response Rate (Hz)")
else:
ax.scatter(baseline_rates, response_rates, c=delta_rate,
s=marker_size, alpha=alpha,
cmap="coolwarm", edgecolors="none",)
cbar = plt.colorbar(ax.collections[0], ax=ax)
cbar.set_label("Δ firing rate (Hz)")
ax.plot([0, max_rate], [0, max_rate],
"k--", alpha=0.5, label="No Change")
ax.set_ylabel("Response Rate (Hz)")
ax.set_xlabel("Baseline Rate (Hz)")
ax.set_xlim(0, max_rate * 1.05)
ax.set_ylim(0, max_rate * 1.05)
ax.set_aspect("equal")
ax.grid(True, alpha=0.3, axis=get_axis(orient))
nucname = "" if nucleus is None else " in " + nucleus
ax.set_title("Rate Change: Baseline vs. Response of " + str(n_neurons) + " neurons" + nucname)
return ax
[docs]
def plot_ratechange(spiketimes_superset, stimulus_onset=None, window=None,
neurons=None, nucleus=None, mode=None, alpha=True):
"""
Visualize Rate Change Scatter of the given neuron population using :py:func:`plot_ratechange_in_ax`.
:param spiketimes_superset: Dictionary returned using :class:`~analyseur.cbgtc.loader.LoadSpikeTimes`
OPTIONAL parameters
:param stimulus_onset: float
:param window: 2-tuple; defines upper and lower range of the bins
:param neurons: "all" or list: range(a, b) or [1, 4, 5, 9]
:param nucleus: string; name of the nucleus
:param mode: "portrait" or None/landscape [default]
:param alpha: `True` [default]
:return: object `ax` with Rate Distribution plotting done into it
.. raw:: html
<hr style="border: 2px solid red; margin: 20px 0;">
"""
if mode == "portrait":
fig, ax = plt.subplots(figsize=(6, 10))
else:
fig, ax = plt.subplots(figsize=(10, 6))
ax = plot_ratechange_in_ax(ax, spiketimes_superset, stimulus_onset=stimulus_onset, window=window,
neurons=neurons, nucleus=nucleus, mode=mode, alpha=alpha)
if ax is None:
print("There are no latencies to plot.")
else:
plt.show()
return fig, ax