Repository: mne-tools/mne-python

Issue:
Cannot use `evoked.animate_topomap()` on CSD data
### Description of the problem

When computing CSD from an EEG dataset, the `animate_topomap` method does not to run. This is most likely a bug, as `plot_topomap` has no such restrictions.

The problem is relatively easy to fix by just adding "csd" [here](https://github.com/mne-tools/mne-python/blob/main/mne/viz/topomap.py#L3253).

The bigger question I suppose is why the checks in `plot_tomap` and `animate_topomap` are different and whether they should be merged to the same. This would account to changing the line  [here](https://github.com/mne-tools/mne-python/blob/main/mne/viz/topomap.py#L3253) to `ch_type = _get_plot_ch_type(evoked, ch_type)`  [such as in the plot_topomap method](https://github.com/mne-tools/mne-python/blob/main/mne/viz/topomap.py#L2129).

### Steps to reproduce


#### This FAILS:

```Python
import mne
from mne.datasets import sample

# load data
data_path = sample.data_path()
meg_path = data_path / "MEG" / "sample"
raw = mne.io.read_raw_fif(meg_path / "sample_audvis_raw.fif")
raw.drop_channels(raw.info["bads"])

# find events
events = mne.find_events(raw)
event_id = {
    "auditory/left": 1,
    "auditory/right": 2,
    "visual/left": 3,
    "visual/right": 4,
    "smiley": 5,
    "button": 32,
}

# only compute with eeg data
raw.pick(["eeg"])

# compute CSD
epochs = mne.Epochs(raw, events, event_id=event_id, tmin=-0.2, tmax=0.5, preload=True)
evoked = epochs["auditory"].average()
evoked_csd = mne.preprocessing.compute_current_source_density(evoked)

# animate topomap
evoked_csd.animate_topomap(times=[-0.1, 0, 0.1])
```

#### This DOES NOT fail

```python
...

evoked_csd.plot_topomap(times=[-0.1, 0, 0.1])
```


### Link to data

_No response_

### Expected results

The animated topomap is shown.

### Actual results

```
Channel type not supported. Supported channel types include 'mag', 'grad', 'eeg'. 'hbo', 'hbr', 'fnirs_cw_amplitude', and 'fnirs_od'.
```

### Additional information

```
Platform             Linux-6.8.6-100.fc38.x86_64-x86_64-with-glibc2.37
Python               3.12.2 (main, Feb 21 2024, 00:00:00) [GCC 13.2.1 20231011 (Red Hat 13.2.1-4)]
Executable           /home/zakm00/git/auditory-stimulation-data-analysis/.venv/bin/python
CPU                   (16 cores)
Memory               15.3 GB

Core
├☒ mne               1.6.1 (outdated, release 1.7.0 is available!)
├☑ numpy             1.26.4 (unknown linalg bindings (threadpoolctl module not found: No module named 'threadpoolctl'))
├☑ scipy             1.13.0
├☑ matplotlib        3.8.3 (backend=module://matplotlib_inline.backend_inline)
├☑ pooch             1.8.1
└☑ jinja2            3.1.3

Numerical (optional)
└☐ unavailable       sklearn, numba, nibabel, nilearn, dipy, openmeeg, cupy, pandas

Visualization (optional)
├☑ pyvista           0.43.5 (OpenGL 4.6 (Core Profile) Mesa 23.1.9 via Mesa Intel(R) Graphics (ADL GT2))
├☑ pyvistaqt         0.11.0
├☑ vtk               9.3.0
├☑ qtpy              2.4.1 (PyQt6=6.6.1)
├☑ ipympl            0.9.4
├☑ pyqtgraph         0.13.6
├☑ mne-qt-browser    0.6.2
├☑ ipywidgets        8.1.2
└☐ unavailable       trame_client, trame_server, trame_vtk, trame_vuetify

Ecosystem (optional)
└☐ unavailable       mne-bids, mne-nirs, mne-features, mne-connectivity, mne-icalabel, mne-bids-pipeline

To update to the latest supported release version to get bugfixes and improvements, visit https://mne.tools/stable/install/updating.html
```


Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴


Resolving patch:
diff --git a/mne/_fiff/pick.py b/mne/_fiff/pick.py
--- a/mne/_fiff/pick.py
+++ b/mne/_fiff/pick.py
@@ -998,7 +998,7 @@ def _picks_by_type(info, meg_combined=False, ref_meg=False, exclude="bads"):
     exclude = _check_info_exclude(info, exclude)
     if meg_combined == "auto":
         meg_combined = _mag_grad_dependent(info)
-    picks_list = []
+
     picks_list = {ch_type: list() for ch_type in _DATA_CH_TYPES_SPLIT}
     for k in range(info["nchan"]):
         if info["chs"][k]["ch_name"] not in exclude:
diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py
--- a/mne/viz/topomap.py
+++ b/mne/viz/topomap.py
@@ -3249,21 +3249,8 @@ def _topomap_animation(
     from matplotlib import pyplot as plt
 
     if ch_type is None:
-        ch_type = _picks_by_type(evoked.info)[0][0]
-    if ch_type not in (
-        "mag",
-        "grad",
-        "eeg",
-        "hbo",
-        "hbr",
-        "fnirs_od",
-        "fnirs_cw_amplitude",
-    ):
-        raise ValueError(
-            "Channel type not supported. Supported channel "
-            "types include 'mag', 'grad', 'eeg'. 'hbo', 'hbr', "
-            "'fnirs_cw_amplitude', and 'fnirs_od'."
-        )
+        ch_type = _get_plot_ch_type(evoked, ch_type)
+
     time_unit, _ = _check_time_unit(time_unit, evoked.times)
     if times is None:
         times = np.linspace(evoked.times[0], evoked.times[-1], 10)
diff --git a/mne/viz/utils.py b/mne/viz/utils.py
--- a/mne/viz/utils.py
+++ b/mne/viz/utils.py
@@ -2821,5 +2821,7 @@ def _get_plot_ch_type(inst, ch_type, allow_ref_meg=False):
                 ch_type = type_
                 break
         else:
-            raise RuntimeError("No plottable channel types found")
+            raise RuntimeError(
+                f"No plottable channel types found. Allowed types are: {allowed_types}"
+            )
     return ch_type


Test patch:
diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py
--- a/mne/viz/tests/test_topomap.py
+++ b/mne/viz/tests/test_topomap.py
@@ -14,6 +14,7 @@
 import matplotlib.pyplot as plt
 import numpy as np
 import pytest
+from matplotlib.colors import PowerNorm, TwoSlopeNorm
 from matplotlib.patches import Circle
 from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal
 
@@ -43,7 +44,11 @@
 )
 from mne.datasets import testing
 from mne.io import RawArray, read_info, read_raw_fif
-from mne.preprocessing import compute_bridged_electrodes
+from mne.preprocessing import (
+    ICA,
+    compute_bridged_electrodes,
+    compute_current_source_density,
+)
 from mne.time_frequency.tfr import AverageTFRArray
 from mne.viz import plot_evoked_topomap, plot_projs_topomap, topomap
 from mne.viz.tests.test_raw import _proj_status
@@ -179,7 +184,21 @@ def test_plot_topomap_animation(capsys):
     anim._func(1)  # _animate has to be tested separately on 'Agg' backend.
     out, _ = capsys.readouterr()
     assert "extrapolation mode local to 0" in out
-    plt.close("all")
+
+
+def test_plot_topomap_animation_csd(capsys):
+    """Test topomap plotting of CSD data."""
+    # evoked
+    evoked = read_evokeds(evoked_fname, "Left Auditory", baseline=(None, 0))
+    evoked_csd = compute_current_source_density(evoked)
+
+    # Test animation
+    _, anim = evoked_csd.animate_topomap(
+        ch_type="csd", times=[0, 0.1], butterfly=False, time_unit="s", verbose="debug"
+    )
+    anim._func(1)  # _animate has to be tested separately on 'Agg' backend.
+    out, _ = capsys.readouterr()
+    assert "extrapolation mode head to 0" in out
 
 
 @pytest.mark.filterwarnings("ignore:.*No contour levels.*:UserWarning")
@@ -190,7 +209,6 @@ def test_plot_topomap_animation_nirs(fnirs_evoked, capsys):
     out, _ = capsys.readouterr()
     assert "extrapolation mode head to 0" in out
     assert len(fig.axes) == 2
-    plt.close("all")
 
 
 def test_plot_evoked_topomap_errors(evoked, monkeypatch):
@@ -553,7 +571,6 @@ def patch():
     orig_bads = evoked_grad.info["bads"]
     evoked_grad.plot_topomap(ch_type="grad", times=[0], time_unit="ms")
     assert_array_equal(evoked_grad.info["bads"], orig_bads)
-    plt.close("all")
 
 
 def test_plot_tfr_topomap():
@@ -685,8 +702,6 @@ def test_plot_topomap_neuromag122():
 
 def test_plot_topomap_bads():
     """Test plotting topomap with bad channels (gh-7213)."""
-    import matplotlib.pyplot as plt
-
     data = np.random.RandomState(0).randn(3, 1000)
     raw = RawArray(data, create_info(3, 1000.0, "eeg"))
     ch_pos_dict = {name: pos for name, pos in zip(raw.ch_names, np.eye(3))}
@@ -695,7 +710,6 @@ def test_plot_topomap_bads():
         raw.info["bads"] = raw.ch_names[:count]
         raw.info._check_consistency()
         plot_topomap(data[:, 0], raw.info)
-    plt.close("all")
 
 
 def test_plot_topomap_channel_distance():
@@ -713,13 +727,10 @@ def test_plot_topomap_channel_distance():
     evoked.set_montage(ten_five)
 
     evoked.plot_topomap(sphere=0.05, res=8)
-    plt.close("all")
 
 
 def test_plot_topomap_bads_grad():
     """Test plotting topomap with bad gradiometer channels (gh-8802)."""
-    import matplotlib.pyplot as plt
-
     data = np.random.RandomState(0).randn(203)
     info = read_info(evoked_fname)
     info["bads"] = ["MEG 2242"]
@@ -727,21 +738,17 @@ def test_plot_topomap_bads_grad():
     info = pick_info(info, picks)
     assert len(info["chs"]) == 203
     plot_topomap(data, info, res=8)
-    plt.close("all")
 
 
 def test_plot_topomap_nirs_overlap(fnirs_epochs):
     """Test plotting nirs topomap with overlapping channels (gh-7414)."""
     fig = fnirs_epochs["A"].average(picks="hbo").plot_topomap()
     assert len(fig.axes) == 5
-    plt.close("all")
 
 
 def test_plot_topomap_nirs_ica(fnirs_epochs):
     """Test plotting nirs ica topomap."""
     pytest.importorskip("sklearn")
-    from mne.preprocessing import ICA
-
     fnirs_epochs = fnirs_epochs.load_data().pick(picks="hbo")
     fnirs_epochs = fnirs_epochs.pick(picks=range(30))
 
@@ -754,7 +761,6 @@ def test_plot_topomap_nirs_ica(fnirs_epochs):
     ica = ICA().fit(fnirs_epochs)
     fig = ica.plot_components()
     assert len(fig[0].axes) == 20
-    plt.close("all")
 
 
 def test_plot_cov_topomap():
@@ -763,13 +769,10 @@ def test_plot_cov_topomap():
     info = read_info(evoked_fname)
     cov.plot_topomap(info)
     cov.plot_topomap(info, noise_cov=cov)
-    plt.close("all")
 
 
 def test_plot_topomap_cnorm():
     """Test colormap normalization."""
-    from matplotlib.colors import PowerNorm, TwoSlopeNorm
-
     rng = np.random.default_rng(42)
     v = rng.uniform(low=-1, high=2.5, size=64)
     v[:3] = [-1, 0, 2.5]