Repository: mne-tools/mne-python

Issue:
_repr_html_ of Info missing many data channels
the _repr_html_ from Info class only shows a subset of channels eg misses iEEG and ECoG. The logic here https://github.com/mne-tools/mne-python/blob/main/mne/io/meas_info.py#L822 should be changed to get all data channels (even all channels) and show the counts.


Discussion:
@vagechirkov Wanna take a stab at this?

Resolving patch:
diff --git a/mne/data/html_templates.py b/mne/data/html_templates.py
--- a/mne/data/html_templates.py
+++ b/mne/data/html_templates.py
@@ -32,14 +32,11 @@
     </tr>
     <tr>
         <th>Good channels</th>
-        <td>{{n_mag}} magnetometer, {{n_grad}} gradiometer,
-            {{n_eeg}} EEG channels, and {{n_fnirs}} fNIRS channels.</td>
+        <td>{{good_channels}}</td>
     </tr>
     <tr>
         <th>Bad channels</th>
-        {{if info['bads'] is not None}}
-        <td>{{', '.join(info['bads'])}}</td>
-        {{else}}<td>None</td>{{endif}}
+        <td>{{bad_channels}}</td>
     </tr>
     <tr>
         <th>EOG channels</th>
diff --git a/mne/defaults.py b/mne/defaults.py
--- a/mne/defaults.py
+++ b/mne/defaults.py
@@ -51,7 +51,9 @@
                 fnirs_fd_ac_amplitude='fNIRS (FD AC amplitude)',
                 fnirs_fd_phase='fNIRS (FD phase)',
                 fnirs_od='fNIRS (OD)', hbr='Deoxyhemoglobin',
-                gof='Goodness of fit', csd='Current source density'),
+                gof='Goodness of fit', csd='Current source density',
+                stim='Stimulus',
+                ),
     mask_params=dict(marker='o',
                      markerfacecolor='w',
                      markeredgecolor='k',
diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py
--- a/mne/io/meas_info.py
+++ b/mne/io/meas_info.py
@@ -40,6 +40,7 @@
 from ._digitization import write_dig as _dig_write_dig
 from .compensator import get_current_comp
 from ..data.html_templates import info_template
+from ..defaults import _handle_default
 
 b = bytes  # alias
 
@@ -608,6 +609,7 @@ def __repr__(self):
         MAX_WIDTH = 68
         strs = ['<Info | %s non-empty values']
         non_empty = 0
+        titles = _handle_default('titles')
         for k, v in self.items():
             if k == 'ch_names':
                 if v:
@@ -659,11 +661,13 @@ def __repr__(self):
             elif isinstance(v, str):
                 entr = shorten(v, MAX_WIDTH, placeholder=' ...')
             elif k == 'chs':
+                # TODO someday we should refactor with _repr_html_ with
+                # bad vs good
                 ch_types = [channel_type(self, idx) for idx in range(len(v))]
                 ch_counts = Counter(ch_types)
-                entr = "%s" % ', '.join("%d %s" % (count, ch_type.upper())
-                                        for ch_type, count
-                                        in ch_counts.items())
+                entr = ', '.join(
+                    f'{count} {titles.get(ch_type, ch_type.upper())}'
+                    for ch_type, count in ch_counts.items())
             elif k == 'custom_ref_applied':
                 entr = str(bool(v))
                 if not v:
@@ -815,24 +819,46 @@ def ch_names(self):
         return self['ch_names']
 
     def _repr_html_(self, caption=None):
+        """Summarize info for HTML representation."""
+        titles = _handle_default('titles')
         if isinstance(caption, str):
             html = f'<h4>{caption}</h4>'
         else:
             html = ''
-        n_eeg = len(pick_types(self, meg=False, eeg=True))
-        n_grad = len(pick_types(self, meg='grad'))
-        n_mag = len(pick_types(self, meg='mag'))
-        n_fnirs = len(pick_types(self, meg=False, eeg=False, fnirs=True))
-        pick_eog = pick_types(self, meg=False, eog=True)
-        if len(pick_eog) > 0:
-            eog = ', '.join(np.array(self['ch_names'])[pick_eog])
-        else:
+
+        # good channels
+        channels = {}
+        ch_types = [channel_type(self, idx) for idx in range(len(self['chs']))]
+        ch_counts = Counter(ch_types)
+        for ch_type, count in ch_counts.items():
+            if ch_type == 'meg':
+                channels['mag'] = len(pick_types(self, meg='mag'))
+                channels['grad'] = len(pick_types(self, meg='grad'))
+            elif ch_type == 'eog':
+                pick_eog = pick_types(self, eog=True)
+                eog = ', '.join(
+                    np.array(self['ch_names'])[pick_eog])
+            elif ch_type == 'ecg':
+                pick_ecg = pick_types(self, ecg=True)
+                ecg = ', '.join(
+                    np.array(self['ch_names'])[pick_ecg])
+            channels[ch_type] = count
+
+        good_channels = ', '.join(
+            [f'{v} {titles.get(k, k.upper())}' for k, v in channels.items()])
+
+        if 'ecg' not in channels.keys():
+            ecg = 'Not available'
+        if 'eog' not in channels.keys():
             eog = 'Not available'
-        pick_ecg = pick_types(self, meg=False, ecg=True)
-        if len(pick_ecg) > 0:
-            ecg = ', '.join(np.array(self['ch_names'])[pick_ecg])
+
+        # bad channels
+        if len(self['bads']) > 0:
+            bad_channels = ', '.join(self['bads'])
         else:
-            ecg = 'Not available'
+            bad_channels = 'None'
+
+        # meas date
         meas_date = self['meas_date']
         if meas_date is not None:
             meas_date = meas_date.strftime("%B %d, %Y  %H:%M:%S") + ' GMT'
@@ -840,12 +866,11 @@ def _repr_html_(self, caption=None):
         if projs:
             projs = '<br/>'.join(
                 p['desc'] + ': o%s' % {0: 'ff', 1: 'n'}[p['active']]
-                for p in projs
-            )
+                for p in projs)
 
         html += info_template.substitute(
-            caption=caption, info=self, meas_date=meas_date, n_eeg=n_eeg,
-            n_grad=n_grad, n_mag=n_mag, n_fnirs=n_fnirs, eog=eog, ecg=ecg,
+            caption=caption, info=self, meas_date=meas_date, ecg=ecg,
+            eog=eog, good_channels=good_channels, bad_channels=bad_channels,
             projs=projs)
         return html
 


Test patch:
diff --git a/mne/io/tests/test_meas_info.py b/mne/io/tests/test_meas_info.py
--- a/mne/io/tests/test_meas_info.py
+++ b/mne/io/tests/test_meas_info.py
@@ -830,6 +830,16 @@ def test_repr_html():
     assert 'Projections' in info._repr_html_()
     info['projs'] = []
     assert 'Projections' not in info._repr_html_()
+    info['bads'] = []
+    assert 'None' in info._repr_html_()
+    info['bads'] = ['MEG 2443', 'EEG 053']
+    assert 'MEG 2443' in info._repr_html_()
+    assert 'EEG 053' in info._repr_html_()
+
+    html = info._repr_html_()
+    for ch in ['204 Gradiometers', '102 Magnetometers', '9 Stimulus',
+               '60 EEG', '1 EOG']:
+        assert ch in html
 
 
 @testing.requires_testing_data
diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py
--- a/mne/io/tests/test_raw.py
+++ b/mne/io/tests/test_raw.py
@@ -253,6 +253,9 @@ def _test_raw_reader(reader, test_preloading=True, test_kwargs=True,
     meas_date = raw.info['meas_date']
     assert meas_date is None or meas_date >= _stamp_to_dt((0, 0))
 
+    # test repr_html
+    assert 'Good channels' in raw.info._repr_html_()
+
     # test resetting raw
     if test_kwargs:
         raw2 = reader(**raw._init_kwargs)