From 318b0ba98e87221aff18486b0c56220456b72827 Mon Sep 17 00:00:00 2001
From: Clemens Brunner <clemens.brunner@gmail.com>
Date: Wed, 18 Mar 2026 17:30:58 +0100
Subject: [PATCH] Backport PR #13746: Improve handling of dropped epochs in
 ica.plot_properties

---
 doc/changes/dev/13746.bugfix.rst |  1 +
 mne/viz/ica.py                   | 43 +++++++++++++++++++++-----------
 mne/viz/tests/test_ica.py        | 15 +++++++++++
 3 files changed, 44 insertions(+), 15 deletions(-)
 create mode 100644 doc/changes/dev/13746.bugfix.rst

diff --git a/doc/changes/dev/13746.bugfix.rst b/doc/changes/dev/13746.bugfix.rst
new file mode 100644
index 00000000000..352b1125972
--- /dev/null
+++ b/doc/changes/dev/13746.bugfix.rst
@@ -0,0 +1 @@
+Improved handling of dropped epochs in :func:`mne.viz.plot_ica_properties` and :meth:`mne.preprocessing.ICA.plot_properties`, allowing plots to be generated even with many bad annotations, by `Clemens Brunner`_.
\ No newline at end of file
diff --git a/mne/viz/ica.py b/mne/viz/ica.py
index 6de278334c4..b1458896e69 100644
--- a/mne/viz/ica.py
+++ b/mne/viz/ica.py
@@ -731,34 +731,47 @@ def _prepare_data_ica_properties(inst, ica, reject_by_annotation=True, reject="a
 
         if reject == "auto":
             reject = ica.reject_
+        drop_inds = None
+        dropped_indices = []
         if reject is None:
-            drop_inds = None
-            dropped_indices = []
-            # break up continuous signal into segments
-            epochs_src = make_fixed_length_epochs(
-                ica.get_sources(inst),
-                duration=2,
-                preload=True,
-                reject_by_annotation=reject_by_annotation,
-                proj=False,
-                verbose=False,
-            )
+            inst_current = inst
         else:
             data = inst.get_data()
             data, drop_inds = _reject_data_segments(
                 data, reject, flat=None, decim=None, info=inst.info, tstep=2.0
             )
-            inst_rejected = RawArray(data, inst.info)
-            # break up continuous signal into segments
+            inst_current = RawArray(data, inst.info)
+        # break up continuous signal into segments; suppress "All epochs were
+        # dropped!" because we handle that case gracefully below
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore", "All epochs were dropped!", RuntimeWarning
+            )
             epochs_src = make_fixed_length_epochs(
-                ica.get_sources(inst_rejected),
+                ica.get_sources(inst_current),
                 duration=2,
                 preload=True,
                 reject_by_annotation=reject_by_annotation,
                 proj=False,
                 verbose=False,
             )
-            # getting dropped epochs indexes
+        # if all epochs were dropped by annotations, stitch the good segments
+        # together so that the plot can still be generated
+        if reject_by_annotation and len(epochs_src) == 0:
+            good_data = inst_current.get_data(reject_by_annotation="omit")
+            min_samples = int(2 * inst.info["sfreq"])
+            if good_data.shape[1] >= min_samples:
+                inst_good = RawArray(good_data, inst_current.info.copy(), verbose=False)
+                epochs_src = make_fixed_length_epochs(
+                    ica.get_sources(inst_good),
+                    duration=2,
+                    preload=True,
+                    reject_by_annotation=False,
+                    proj=False,
+                    verbose=False,
+                )
+        # getting dropped epochs indexes
+        if drop_inds is not None:
             dropped_indices = [(d[0] // len(epochs_src.times)) + 1 for d in drop_inds]
         kind = "Segment"
     else:
diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py
index 06e78df85cf..eaeb97dfcf5 100644
--- a/mne/viz/tests/test_ica.py
+++ b/mne/viz/tests/test_ica.py
@@ -265,6 +265,21 @@ def test_plot_ica_properties():
     # don't drop
     ica.plot_properties(raw_annot, reject_by_annotation=False, **topoargs)
 
+    # test fallback when ALL 2-second epoch windows are contaminated by annotations: one
+    # bad segment per window so every epoch is dropped, but the inter-annotation gaps
+    # stitch together to >= 2 s of clean data.
+    annot_all_bad = Annotations(
+        onset=[0.5, 2.5, 4.5, 6.5],
+        duration=[1.0, 1.0, 1.0, 1.0],
+        description=["BAD"] * 4,
+    )
+    raw_all_bad = _get_raw(preload=True).set_annotations(annot_all_bad).crop(0, 8)
+    raw_all_bad.pick(np.arange(10))
+    raw_all_bad.del_proj()
+    fig = ica.plot_properties(raw_all_bad, picks=[0], **topoargs)
+    assert_equal(len(fig), 1)
+    plt.close("all")
+
 
 def test_plot_ica_sources(raw_orig, browser_backend, monkeypatch):
     """Test plotting of ICA panel."""
