Skip to content

Commit

Permalink
scale figsize in plot_events depending on n unique events (#12844)
Browse files Browse the repository at this point in the history
  • Loading branch information
sappelhoff authored Sep 20, 2024
1 parent 6f9646f commit a218f96
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 19 deletions.
1 change: 1 addition & 0 deletions doc/changes/devel/12844.other.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Improve automatic figure scaling of :func:`mne.viz.plot_events`, and event_id and count overview legend when a high amount of unique events is supplied, by `Stefan Appelhoff`_.
2 changes: 1 addition & 1 deletion examples/datasets/limo_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
print(limo_epochs.metadata.head())

# %%
# Now let's take a closer look at the information in the epochs
# Now let us take a closer look at the information in the epochs
# metadata.

# We want include all columns in the summary table
Expand Down
4 changes: 2 additions & 2 deletions examples/preprocessing/epochs_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
#
# All experimental events are stored in the :class:`~mne.io.Raw` instance as
# :class:`~mne.Annotations`. We first need to convert these to events and the
# corresponding mapping from event codes to event names (``event_id``). We then
# visualize the events.
# corresponding mapping from event codes to event names (``event_id``).
# We then visualize the events.
all_events, all_event_id = mne.events_from_annotations(raw)
mne.viz.plot_events(events=all_events, event_id=all_event_id, sfreq=raw.info["sfreq"])

Expand Down
37 changes: 32 additions & 5 deletions mne/viz/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -841,12 +841,18 @@ def plot_events(
color = _handle_event_colors(color, unique_events, event_id)
import matplotlib.pyplot as plt

unique_events_id = np.array(unique_events_id)

fig = None
figsize = plt.rcParams["figure.figsize"]
# assuming the user did not change matplotlib default params, the figsize of
# (6.4, 4.8) becomes too big if scaled beyond twice its size, so maximum 2
_scaling = min(max(1, len(unique_events_id) / 10), 2)
figsize_scaled = np.array(figsize) * _scaling
if axes is None:
fig = plt.figure(layout="constrained")
fig = plt.figure(layout="constrained", figsize=tuple(figsize_scaled))
ax = axes if axes else plt.gca()

unique_events_id = np.array(unique_events_id)
min_event = np.min(unique_events_id)
max_event = np.max(unique_events_id)
max_x = (
Expand All @@ -861,9 +867,9 @@ def plot_events(
continue
y = np.full(count, idx + 1 if equal_spacing else events[ev_mask, 2][0])
if event_id is not None:
event_label = f"{event_id_rev[ev]} ({count})"
event_label = f"{event_id_rev[ev]}\n(id:{ev}; N:{count})"
else:
event_label = f"N={count:d}"
event_label = f"id:{ev}; N:{count:d}"
labels.append(event_label)
kwargs = {}
if ev in color:
Expand Down Expand Up @@ -893,11 +899,32 @@ def plot_events(
# reverse order so that the highest numbers are at the top
# (match plot order)
handles, labels = handles[::-1], labels[::-1]

# spread legend entries over more columns, 25 still ~fit in one column
# (assuming non-user supplied fig)
ncols = int(np.ceil(len(unique_events_id) / 25))

# Make space for legend
box = ax.get_position()
factor = 0.8 if event_id is not None else 0.9
factor -= 0.1 * (ncols - 1)
ax.set_position([box.x0, box.y0, box.width * factor, box.height])

# Try some adjustments to squeeze as much information into the legend
# without cutting off the ends
ax.legend(
handles, labels, loc="center left", bbox_to_anchor=(1, 0.5), fontsize="small"
handles,
labels,
loc="center left",
bbox_to_anchor=(1, 0.5),
fontsize="small",
borderpad=0, # default 0.4
labelspacing=0.25, # default 0.5
columnspacing=1.0, # default 2
handletextpad=0, # default 0.8
markerscale=2, # default 1
borderaxespad=0.2, # default 0.5
ncols=ncols,
)
fig.canvas.draw()
plt_show(show)
Expand Down
8 changes: 4 additions & 4 deletions tutorials/clinical/60_sleep.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,22 @@
# Load the data
# -------------
#
# Here we download the data from two subjects and the end goal is to obtain
# :term:`epochs` and its associated ground truth.
# Here we download the data of two subjects. The end goal is to obtain
# :term:`epochs` and the associated ground truth.
#
# MNE-Python provides us with
# :func:`mne.datasets.sleep_physionet.age.fetch_data` to conveniently download
# data from the Sleep Physionet dataset
# :footcite:`KempEtAl2000,GoldbergerEtAl2000`.
# Given a list of subjects and records, the fetcher downloads the data and
# provides us for each subject, a pair of files:
# provides us with a pair of files for each subject:
#
# * ``-PSG.edf`` containing the polysomnography. The :term:`raw` data from the
# EEG helmet,
# * ``-Hypnogram.edf`` containing the :term:`annotations` recorded by an
# expert.
#
# Combining these two in a :class:`mne.io.Raw` object then we can extract
# Combining these two in a :class:`mne.io.Raw` object will allow us to extract
# :term:`events` based on the descriptions of the annotations to obtain the
# :term:`epochs`.
#
Expand Down
2 changes: 1 addition & 1 deletion tutorials/intro/10_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
# example of this is shown in the next section. There is also a convenient
# `~mne.viz.plot_events` function for visualizing the distribution of events
# across the duration of the recording (to make sure event detection worked as
# expected). Here we'll also make use of the `~mne.Info` attribute to get the
# expected). Here we will also make use of the `~mne.Info` attribute to get the
# sampling frequency of the recording (so our x-axis will be in seconds instead
# of in samples).

Expand Down
8 changes: 4 additions & 4 deletions tutorials/preprocessing/70_fnirs_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@
# and the unwanted heart rate component has been removed, we can extract epochs
# related to each of the experimental conditions.
#
# First we extract the events of interest and visualise them to ensure they are
# First we extract the events of interest and visualize them to ensure they are
# correct.

events, event_dict = mne.events_from_annotations(raw_haemo)
Expand All @@ -181,7 +181,7 @@

# %%
# Next we define the range of our epochs, the rejection criteria,
# baseline correction, and extract the epochs. We visualise the log of which
# baseline correction, and extract the epochs. We visualize the log of which
# epochs were dropped.

reject_criteria = dict(hbo=80e-6)
Expand Down Expand Up @@ -209,7 +209,7 @@
# -------------------------------------------
#
# Now we can view the haemodynamic response for our tapping condition.
# We visualise the response for both the oxy- and deoxyhaemoglobin, and
# We visualize the response for both the oxy- and deoxyhaemoglobin, and
# observe the expected peak in HbO at around 6 seconds consistently across
# trials, and the consistent dip in HbR that is slightly delayed relative to
# the HbO peak.
Expand Down Expand Up @@ -296,7 +296,7 @@
# ---------------------------------------
#
# Finally we generate topo maps for the left and right conditions to view
# the location of activity. First we visualise the HbO activity.
# the location of activity. First we visualize the HbO activity.

times = np.arange(4.0, 11.0, 1.0)
epochs["Tapping/Left"].average(picks="hbo").plot_topomap(times=times, **topomap_args)
Expand Down
4 changes: 2 additions & 2 deletions tutorials/raw/20_event_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@
# :func:`mne.viz.plot_events` will plot each event versus its sample number
# (or, if you provide the sampling frequency, it will plot them versus time in
# seconds). It can also account for the offset between sample number and sample
# index in Neuromag systems, with the ``first_samp`` parameter. If an event
# dictionary is provided, it will be used to generate a legend:
# index in Neuromag systems, with the ``first_samp`` parameter.
# If an event dictionary is provided, it will be used to generate a legend:

fig = mne.viz.plot_events(
events, sfreq=raw.info["sfreq"], first_samp=raw.first_samp, event_id=event_dict
Expand Down

0 comments on commit a218f96

Please sign in to comment.