Repository: mne-tools/mne-python

Issue:
ICA.plot_overlay not useful for average-referenced EEG data
The lower panel of `ICA.plot_overlay` figures shows a trace of all channels averaged together (before and after ICA). For average-referenced EEG data, the mean of all channels is very very close to zero (on the order of 1e-20 usually) and thus the plot basically shows noise. We should figure out an elegant way to handle this. Possibilities:

- detect avg ref and then warn?
- detect avg ref and do something other than take the mean of all channels?
- do something other than take the mean of all channels *in all cases*?

I haven't thought much about what is the sensible path here, just documenting this so we don't lose track of it.

Related forum post: https://mne.discourse.group/t/ica-plot-overlay-gives-strange-results/7207/10

cc @mscheltienne who did the legwork figuring out what was going on with the user's plots. 🙏🏻 


Discussion:
Thanks for documenting! 
I'll add that we have the usual problem, we don't know what is the EEG reference, even after calling set_eeg_reference. 
> The lower panel of ICA.plot_overlay figures shows a trace of all channels averaged together (before and after ICA)
>
> do something other than take the mean of all channels in all cases?

+1 for this -- I'm a bit surprised it's the average because polarity differences between channels would lead to cancellation even without any processing, making it look like activity is small when it could be huge. Something like the standard deviation/GFP seems much more informative.
+1 for using RMS for MEG and GFP for EEG
> +1 for using RMS for MEG and GFP for EEG

SGTM, @mscheltienne do you want to tackle this one?
RMS and GFP sounds good. I can add that to my to-do list for next week.
One more point I have a bit of an hard time to wrap my head around, and I'd prefer to figure it out before starting to change the viz functions of the ICA. What about ICA decomposition on combined MEG/EEG (or any combined data types)? I don't have an issue with a fit on the entire data array at once, but the visualization of the mixing matrix seems misleading.

```
from mne.datasets import sample
from mne.io import read_raw_fif
from mne.preprocessing import ICA


fname = sample.data_path() / "MEG" / "sample" / "sample_audvis_raw.fif"
raw = read_raw_fif(fname, preload=False)
raw.crop(tmin=0, tmax=10).pick(["meg", "eeg"])
raw.load_data()
raw.filter(1., 40.)

ica = ICA(n_components=0.95, method="picard", max_iter=10)
ica.fit(raw)
```

The code snippet fits 115 components on 204 gradiometers, 102 magnetometers and 60 EEG channels. I can now do `ica.plot_components(ch_type="eeg")` and get 115 topographies labeled "eeg". Shouldn't I be getting maximum 60 topographies, depending on the PCA step? My point is, shouldn't the mixing matrix be divided by channel types during visualization? From the product of the mixing matrix and to the PCA components `data = np.dot(ica.mixing_matrix_.T, ica.pca_components_[:ica.n_components_])` of shape `(n_components, n_channels)`, I can select the channels but I am still left with the full number of components 115. Some of them should have very small weights/eigenvalue and should be disregarded during visualization. Am I missing something?

```
import numpy as np
from matplotlib import pyplot as plt
from mne.datasets import sample
from mne.io import read_raw_fif
from mne.io.pick import _picks_to_idx
from mne.preprocessing import ICA


fname = sample.data_path() / "MEG" / "sample" / "sample_audvis_raw.fif"
raw = read_raw_fif(fname, preload=False)
raw.crop(tmin=0, tmax=30).pick(["meg", "eeg"])
raw.load_data()
raw.filter(1., 40.)

ica = ICA(n_components=0.95, method="picard")
ica.fit(raw)

picks = _picks_to_idx(ica.info, picks="eeg")
data = np.dot(ica.mixing_matrix_.T, ica.pca_components_[:ica.n_components_])
f, ax = plt.subplots()
img = ax.imshow(data[:, picks], cmap="Reds", vmin=0, vmax=0.1)
ax.set_ylabel("n_components")
ax.set_xlabel("n_channels")
f.colorbar(img)
```

![Screenshot from 2023-07-20 15-41-04](https://github.com/mne-tools/mne-python/assets/73893616/a3347e43-479d-4043-8161-c470af6f4927)

EDIT: Not sure about the independence of sources and how specifically an IC will capture a single channel type.

Other issue related to this point: https://github.com/mne-tools/mne-icalabel/issues/133
I will add some additional thoughts to my previous comment. If the ICA was fitted on 3 modalites, 204 gradiometers, 102 magnetometers and 60 EEG channels, it is possible to have:
- a component which contributes to all 3 modalities, e.g. the eye-blink.
- a component which does not contribute to a modality, e.g. a component which doesn't contribute to the EEG as shown above.

I find misleading to display with `plot_components` a single modality, ordered by *total explained variance* instead of explained variance *within* the modality. One might remove an IC thinking it's an artifact in the plotted modality while it is in fact not contributing to it and was capturing another.

One improvement to the topographic plot of a component within `ica.plot_components` and `ica.plot_properties` could be to plot the topographies of all modalities together with the explained variance of the components for each modality.

Anyway, I am sidetracking quite a bit from the original issue, I got a bit lost in the ICA code.. but I think `ica.plot_overlay` should also be splitting the modalities into different plots.
> I can now do `ica.plot_components(ch_type="eeg")` and get 115 topographies labeled "eeg". Shouldn't I be getting maximum 60 topographies, depending on the PCA step?

@cbrnr and I ran into a potentially related bug during last week's workshop:
We fit ICA on M/EEG data, ran `plot_components(inst=epochs, ch_type="eeg")`, and while we were seeing EEG topos, right-clicking on any one of them brought up a properties plot for the respective `mag`-based data – not `eeg`, as was expected.
Seems like the `ch_type` argument does not even exist for `plot_properties`. It should be forwarded all the way here: https://github.com/mne-tools/mne-python/blob/9b3a4d38a252e8e26980135229f68ca90d0fe359/mne/viz/ica.py#L211
@mscheltienne sounds like you are just about ready to open a PR :smile: 

Resolving patch:
diff --git a/mne/viz/ica.py b/mne/viz/ica.py
--- a/mne/viz/ica.py
+++ b/mne/viz/ica.py
@@ -26,9 +26,10 @@
 from .evoked import _butterfly_on_button_press, _butterfly_onpick
 from ..channels.channels import _get_ch_type
 from ..utils import _validate_type, fill_doc
-from ..defaults import _handle_default
+from ..defaults import _handle_default, DEFAULTS
 from ..io.meas_info import create_info
 from ..io.pick import pick_types, _picks_to_idx
+from ..io.proj import _has_eeg_average_ref_proj
 from ..utils import _reject_data_segments, verbose
 
 
@@ -49,7 +50,7 @@ def plot_ica_sources(
     use_opengl=None,
     *,
     theme=None,
-    overview_mode=None
+    overview_mode=None,
 ):
     """Plot estimated latent sources given the unmixing matrix.
 
@@ -233,7 +234,7 @@ def _plot_ica_properties(
         combine=None,
         colorbar=False,
         show=False,
-        **image_args
+        **image_args,
     )
 
     # spectrum
@@ -403,7 +404,7 @@ def plot_ica_properties(
     reject="auto",
     reject_by_annotation=True,
     *,
-    verbose=None
+    verbose=None,
 ):
     """Display component properties.
 
@@ -521,7 +522,7 @@ def _fast_plot_ica_properties(
     precomputed_data=None,
     reject_by_annotation=True,
     *,
-    verbose=None
+    verbose=None,
 ):
     """Display component properties."""
     from ..preprocessing import ICA
@@ -1027,7 +1028,7 @@ def plot_ica_overlay(
     n_pca_components=None,
     *,
     on_baseline="warn",
-    verbose=None
+    verbose=None,
 ):
     """Overlay of raw and cleaned signals given the unmixing matrix.
 
@@ -1038,14 +1039,13 @@ def plot_ica_overlay(
     ica : instance of mne.preprocessing.ICA
         The ICA object.
     inst : instance of Raw or Evoked
-        The signal to plot. If `~mne.io.Raw`, the raw data is displayed before
-        and after cleaning. In a second panel, the cross-channel average will
-        be displayed. Since dipolar sources will be canceled out, this
-        representation is sensitive to artifacts. If `~mne.Evoked`, butterfly
-        traces for signals before and after cleaning will be superimposed.
+        The signal to plot. If `~mne.io.Raw`, the raw data per channel type is displayed
+        before and after cleaning. A second panel with the RMS for MEG sensors and the
+        :term:`GFP` for EEG sensors is displayed. If `~mne.Evoked`, butterfly traces for
+        signals before and after cleaning will be superimposed.
     exclude : array-like of int | None (default)
-        The components marked for exclusion. If ``None`` (default), ICA.exclude
-        will be used.
+        The components marked for exclusion. If ``None`` (default), the components
+        listed in ``ICA.exclude`` will be used.
     %(picks_base)s all channels that were included during fitting.
     start, stop : float | None
        The first and last time point (in seconds) of the data to plot. If
@@ -1079,19 +1079,14 @@ def plot_ica_overlay(
         title = "Signals before (red) and after (black) cleaning"
     picks = ica.ch_names if picks is None else picks
     picks = _picks_to_idx(inst.info, picks, exclude=())
-    ch_types_used = inst.get_channel_types(picks=picks, unique=True)
     if exclude is None:
         exclude = ica.exclude
     if not isinstance(exclude, (np.ndarray, list)):
         raise TypeError("exclude must be of type list. Got %s" % type(exclude))
     if isinstance(inst, BaseRaw):
-        if start is None:
-            start = 0.0
-        if stop is None:
-            stop = 3.0
-        start_compare, stop_compare = _check_start_stop(inst, start, stop)
-        data, times = inst[picks, start_compare:stop_compare]
-
+        start = 0.0 if start is None else start
+        stop = 3.0 if stop is None else stop
+        start, stop = _check_start_stop(inst, start, stop)
         raw_cln = ica.apply(
             inst.copy(),
             exclude=exclude,
@@ -1099,13 +1094,13 @@ def plot_ica_overlay(
             stop=stop,
             n_pca_components=n_pca_components,
         )
-        data_cln, _ = raw_cln[picks, start_compare:stop_compare]
         fig = _plot_ica_overlay_raw(
-            data=data,
-            data_cln=data_cln,
-            times=times,
+            raw=inst,
+            raw_cln=raw_cln,
+            picks=picks,
+            start=start,
+            stop=stop,
             title=title,
-            ch_types_used=ch_types_used,
             show=show,
         )
     else:
@@ -1128,15 +1123,23 @@ def plot_ica_overlay(
     return fig
 
 
-def _plot_ica_overlay_raw(data, data_cln, times, title, ch_types_used, show):
+def _plot_ica_overlay_raw(*, raw, raw_cln, picks, start, stop, title, show):
     """Plot evoked after and before ICA cleaning.
 
     Parameters
     ----------
-    ica : instance of mne.preprocessing.ICA
-        The ICA object.
-    epochs : instance of mne.Epochs
-        The Epochs to be regarded.
+    raw : Raw
+        Raw data before exclusion of ICs.
+    raw_cln : Raw
+        Data after exclusion of ICs.
+    picks : array of shape (n_channels_selected,)
+        Array of selected channel indices.
+    start : int
+        Start time to plot in samples.
+    stop : int
+        Stop time to plot in samples.
+    title : str
+        Title of the figure(s).
     show : bool
         Show figure if True.
 
@@ -1146,26 +1149,55 @@ def _plot_ica_overlay_raw(data, data_cln, times, title, ch_types_used, show):
     """
     import matplotlib.pyplot as plt
 
-    # Restore sensor space data and keep all PCA components
-    # let's now compare the date before and after cleaning.
-    # first the raw data
-    assert data.shape == data_cln.shape
-    fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
-    plt.suptitle(title)
-    ax1.plot(times, data.T, color="r")
-    ax1.plot(times, data_cln.T, color="k")
-    ax1.set(xlabel="Time (s)", xlim=times[[0, -1]], title="Raw data")
-
-    _ch_types = {"mag": "Magnetometers", "grad": "Gradiometers", "eeg": "EEG"}
-    ch_types = ", ".join([_ch_types[k] for k in ch_types_used])
-    ax2.set_title("Average across channels ({})".format(ch_types))
-    ax2.plot(times, data.mean(0), color="r")
-    ax2.plot(times, data_cln.mean(0), color="k")
-    ax2.set(xlabel="Time (s)", xlim=times[[0, -1]])
-    tight_layout(fig=fig)
-
-    fig.subplots_adjust(top=0.90)
-    fig.canvas.draw()
+    ch_types = raw.get_channel_types(picks=picks, unique=True)
+    for ch_type in ch_types:
+        if ch_type in ("mag", "grad"):
+            fig, ax = plt.subplots(3, 1, sharex=True, constrained_layout=True)
+        elif ch_type == "eeg" and not _has_eeg_average_ref_proj(
+            raw.info, check_active=True
+        ):
+            fig, ax = plt.subplots(3, 1, sharex=True, constrained_layout=True)
+        else:
+            fig, ax = plt.subplots(2, 1, sharex=True, constrained_layout=True)
+        fig.suptitle(title)
+
+        # select sensors and retrieve data array
+        picks_by_type = _picks_to_idx(raw.info, ch_type, exclude=())
+        picks_ = np.intersect1d(picks, picks_by_type)
+        data, times = raw[picks_, start:stop]
+        data_cln, _ = raw_cln[picks_, start:stop]
+
+        # plot all sensors of the same type together
+        ax[0].plot(times, data.T, color="r")
+        ax[0].plot(times, data_cln.T, color="k")
+        _ch_type = DEFAULTS["titles"].get(ch_type, ch_type)
+        ax[0].set(xlabel="Time (s)", xlim=times[[0, -1]], title=f"Raw {_ch_type} data")
+
+        # second plot for M/EEG using GFP or RMS
+        if ch_type == "eeg":  # Global Field Power
+            ax[1].plot(times, np.std(data, axis=0), color="r")
+            ax[1].plot(times, np.std(data_cln, axis=0), color="k")
+            ax[1].set(
+                xlabel="Time (s)",
+                xlim=times[[0, -1]],
+                title=f"{_ch_type} Global Field Power",
+            )
+        elif ch_type in ("mag", "grad"):  # RMS
+            ax[1].plot(times, np.sqrt((data**2).mean(axis=0)), color="r")
+            ax[1].plot(times, np.sqrt((data_cln**2).mean(axis=0)), color="k")
+            ax[1].set(xlabel="Time (s)", xlim=times[[0, -1]], title=f"{_ch_type} RMS")
+
+        # last plot with the average across all channels of the same type
+        if ch_type != "eeg" or not _has_eeg_average_ref_proj(
+            raw.info, check_active=True
+        ):
+            ax[-1].plot(times, data.mean(axis=0), color="r")
+            ax[-1].plot(times, data_cln.mean(axis=0), color="k")
+            ax[-1].set(
+                xlabel="Time (s)",
+                xlim=times[[0, -1]],
+                title=f"Average across {_ch_type} channels",
+            )
     plt_show(show)
     return fig
 
@@ -1195,9 +1227,7 @@ def _plot_ica_overlay_evoked(evoked, evoked_cln, title, show):
     ch_types_used_cln = [c for c in ["mag", "grad", "eeg"] if c in evoked_cln]
 
     if len(ch_types_used) != len(ch_types_used_cln):
-        raise ValueError(
-            "Raw and clean evokeds must match. " "Found different channels."
-        )
+        raise ValueError("Raw and clean evokeds must match. Found different channels.")
 
     fig, axes = plt.subplots(n_rows, 1)
     if title is None:
@@ -1236,7 +1266,7 @@ def _plot_sources(
     use_opengl,
     *,
     theme=None,
-    overview_mode=None
+    overview_mode=None,
 ):
     """Plot the ICA components as a RawArray or EpochsArray."""
     from ._figure import _get_browser
diff --git a/tutorials/preprocessing/40_artifact_correction_ica.py b/tutorials/preprocessing/40_artifact_correction_ica.py
--- a/tutorials/preprocessing/40_artifact_correction_ica.py
+++ b/tutorials/preprocessing/40_artifact_correction_ica.py
@@ -5,9 +5,9 @@
 Repairing artifacts with ICA
 ============================
 
-This tutorial covers the basics of independent components analysis (ICA) and
-shows how ICA can be used for artifact repair; an extended example illustrates
-repair of ocular and heartbeat artifacts. For conceptual background on ICA, see
+This tutorial covers the basics of independent components analysis (ICA) and shows how
+ICA can be used for artifact repair; an extended example illustrates repair of ocular
+and heartbeat artifacts. For conceptual background on ICA, see
 :ref:`this scikit-learn tutorial
 <sphx_glr_auto_examples_decomposition_plot_ica_blind_source_separation.py>`.
 


Test patch: