Repository: mne-tools/mne-python

Issue:
question: puzzling results in epochs.plot_topo_image
Setup:
```
import os
import mne
sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, preload=True, verbose=False)
events = mne.find_events(raw, stim_channel='STI 014')
epochs = mne.Epochs(raw, events, preload=True)
```
I expected these two options to yield the same results, but they're very different:
```
# OPTION 1
epochs.copy().pick_types(meg=False, eeg=True).plot_topo_image()
```
![Figure_1](https://user-images.githubusercontent.com/1810515/55372922-db7e2c80-54af-11e9-9e52-503aa2065d51.png)
```
# OPTION 2
eeg_layout = mne.channels.find_layout(raw.info, ch_type='eeg')                                                            
epochs.plot_topo_image(layout=eeg_layout)
```
![Figure_2](https://user-images.githubusercontent.com/1810515/55372927-e20ca400-54af-11e9-9d20-07cce3152ecc.png)

Is the difference here intentional, or a bug?


Discussion:
Looks like a scaling / vmiv / vmax problem in the first example
So, not intentional I take it. I'll have a look.
For the purpose of DRY, this could default to #6078 ...
@jona-sassenhagen it looks like `_plot_masked_image` (and its proposed public version) require/create an axes object each time called, whereas `plot_topo_image_epochs` does the `_iter_topography` thing to plot each little imagmap inside one big axes.  So changing `plot_topo_image_epochs` to use `_plot_masked_image` behind the scenes might take some heavy lifting.  

(as an aside, I noticed that `_plot_masked_image` has a `picks` argument that appears to go unused?)

Hm? iter_topography creates one axis per sensor, right?

https://github.com/mne-tools/mne-python/blob/9b5ae5104ecdeaea13a88273c712382bf131162c/mne/viz/topo.py#L214

So this would essentially be about changing the show_func mostly, right?
Not saying it is trivial.

Either way, the same applies to https://github.com/mne-tools/mne-python/issues/4746, which I will do during the sprint.
ah, you're right @jona-sassenhagen.  I thought it was passing `unified=True` by default, but that must have been something else I was looking at yesterday.
@larsoner I'm convinced that in fact both of the plots above have messed up vmin/vmax. Compare the detail plots:

```
epochs.plot_image(picks='EEG 060') 
```
![Figure_3](https://user-images.githubusercontent.com/1810515/55432352-22ffc980-553f-11e9-9b3a-c9fd43cffce9.png)

versus clicking on that channel after
```
epochs.copy().pick_types(meg=False, eeg=True).plot_topo_image()
```
![Figure_5](https://user-images.githubusercontent.com/1810515/55432395-41fe5b80-553f-11e9-852b-e1ca90d54e48.png)

versus clicking on that channel after 
```
epochs.plot_topo_image(layout=eeg_layout)
```
![Figure_4](https://user-images.githubusercontent.com/1810515/55432420-4c205a00-553f-11e9-86fe-1ba249c4181d.png)

Still working out how to correct it.
I remember fixing a lot of code in epochs.plot_image to get the vlims right. Maybe check there.

Or I could indeed try to hook this up to _plot_masked_image at the sprint, so it could simply share that vlim functionality.
I'm gonna punt on this one until the sprint, I think. I put in a draft PR for what I think should be happening, but the resulting plots don't look right (too much of the signal lands near zero => images are mostly grey/white).  I also considered an approach using `np.quantile` to set vmin/vmax at the (0.01, 0.99) quantiles to pull some color into the middle range... maybe worth exploring?
I've had this problem too some time ago, but now I rarely use `plot_topo_image`.
Mayyyyybe with #6186 in we can use _set_image_lims_and_scale for this.

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -958,7 +958,7 @@ def plot_psd_topomap(self, bands=None, vmin=None, vmax=None, tmin=None,
 
     @copy_function_doc_to_method_doc(plot_topo_image_epochs)
     def plot_topo_image(self, layout=None, sigma=0., vmin=None, vmax=None,
-                        colorbar=True, order=None, cmap='RdBu_r',
+                        colorbar=None, order=None, cmap='RdBu_r',
                         layout_scale=.95, title=None, scalings=None,
                         border='none', fig_facecolor='k', fig_background=None,
                         font_color='w', show=True):
diff --git a/mne/viz/topo.py b/mne/viz/topo.py
--- a/mne/viz/topo.py
+++ b/mne/viz/topo.py
@@ -187,7 +187,7 @@ def _plot_topo(info, times, show_func, click_func=None, layout=None,
     click_func = show_func if click_func is None else click_func
     on_pick = partial(click_func, tmin=tmin, tmax=tmax, vmin=vmin,
                       vmax=vmax, ylim=ylim, x_label=x_label,
-                      y_label=y_label, colorbar=colorbar)
+                      y_label=y_label)
 
     if axes is None:
         fig = plt.figure()
@@ -481,11 +481,13 @@ def _plot_timeseries_unified(bn, ch_idx, tmin, tmax, vmin, vmax, ylim, data,
 def _erfimage_imshow(ax, ch_idx, tmin, tmax, vmin, vmax, ylim=None, data=None,
                      epochs=None, sigma=None, order=None, scalings=None,
                      vline=None, x_label=None, y_label=None, colorbar=False,
-                     cmap='RdBu_r'):
+                     cmap='RdBu_r', vlim_array=None):
     """Plot erfimage on sensor topography."""
     from scipy import ndimage
     import matplotlib.pyplot as plt
-    this_data = data[:, ch_idx, :].copy() * scalings[ch_idx]
+    this_data = data[:, ch_idx, :]
+    if vlim_array is not None:
+        vmin, vmax = vlim_array[ch_idx]
 
     if callable(order):
         order = order(epochs.times, this_data)
@@ -512,7 +514,8 @@ def _erfimage_imshow(ax, ch_idx, tmin, tmax, vmin, vmax, ylim=None, data=None,
 def _erfimage_imshow_unified(bn, ch_idx, tmin, tmax, vmin, vmax, ylim=None,
                              data=None, epochs=None, sigma=None, order=None,
                              scalings=None, vline=None, x_label=None,
-                             y_label=None, colorbar=False, cmap='RdBu_r'):
+                             y_label=None, colorbar=False, cmap='RdBu_r',
+                             vlim_array=None):
     """Plot erfimage topography using a single axis."""
     from scipy import ndimage
     _compute_scalings(bn, (tmin, tmax), (0, len(epochs.events)))
@@ -520,7 +523,8 @@ def _erfimage_imshow_unified(bn, ch_idx, tmin, tmax, vmin, vmax, ylim=None,
     data_lines = bn.data_lines
     extent = (bn.x_t + bn.x_s * tmin, bn.x_t + bn.x_s * tmax, bn.y_t,
               bn.y_t + bn.y_s * len(epochs.events))
-    this_data = data[:, ch_idx, :].copy() * scalings[ch_idx]
+    this_data = data[:, ch_idx, :]
+    vmin, vmax = (None, None) if vlim_array is None else vlim_array[ch_idx]
 
     if callable(order):
         order = order(epochs.times, this_data)
@@ -798,7 +802,7 @@ def _plot_update_evoked_topo_proj(params, bools):
 
 
 def plot_topo_image_epochs(epochs, layout=None, sigma=0., vmin=None,
-                           vmax=None, colorbar=True, order=None, cmap='RdBu_r',
+                           vmax=None, colorbar=None, order=None, cmap='RdBu_r',
                            layout_scale=.95, title=None, scalings=None,
                            border='none', fig_facecolor='k',
                            fig_background=None, font_color='w', show=True):
@@ -806,7 +810,7 @@ def plot_topo_image_epochs(epochs, layout=None, sigma=0., vmin=None,
 
     Parameters
     ----------
-    epochs : instance of Epochs
+    epochs : instance of :class:`~mne.Epochs`
         The epochs.
     layout: instance of Layout
         System specific sensor positions.
@@ -819,8 +823,9 @@ def plot_topo_image_epochs(epochs, layout=None, sigma=0., vmin=None,
     vmax : float
         The max value in the image. The unit is uV for EEG channels,
         fT for magnetometers and fT/cm for gradiometers.
-    colorbar : bool
-        Display or not a colorbar.
+    colorbar : bool | None
+        Whether to display a colorbar or not. If ``None`` a colorbar will be
+        shown only if all channels are of the same type. Defaults to ``None``.
     order : None | array of int | callable
         If not None, order is used to reorder the epochs on the y-axis
         of the image. If it's an array of int it should be of length
@@ -836,40 +841,69 @@ def plot_topo_image_epochs(epochs, layout=None, sigma=0., vmin=None,
         Title of the figure.
     scalings : dict | None
         The scalings of the channel types to be applied for plotting. If
-        None, defaults to `dict(eeg=1e6, grad=1e13, mag=1e15)`.
+        ``None``, defaults to `dict(eeg=1e6, grad=1e13, mag=1e15)`.
     border : str
         matplotlib borders style to be used for each sensor plot.
     fig_facecolor : color
         The figure face color. Defaults to black.
     fig_background : None | array
         A background image for the figure. This must be a valid input to
-        `matplotlib.pyplot.imshow`. Defaults to None.
+        :func:`matplotlib.pyplot.imshow`. Defaults to ``None``.
     font_color : color
         The color of tick labels in the colorbar. Defaults to white.
     show : bool
-        Show figure if True.
+        Whether to show the figure. Defaults to ``True``.
 
     Returns
     -------
-    fig : instance of matplotlib.figure.Figure
+    fig : instance of :class:`matplotlib.figure.Figure`
         Figure distributing one image per channel across sensor topography.
+
+    Notes
+    -----
+    In an interactive Python session, this plot will be interactive; clicking
+    on a channel image will pop open a larger view of the image; this image
+    will always have a colorbar even when the topo plot does not (because it
+    shows multiple sensor types).
     """
     scalings = _handle_default('scalings', scalings)
-    data = epochs.get_data()
-    scale_coeffs = list()
-    for idx in range(epochs.info['nchan']):
-        ch_type = channel_type(epochs.info, idx)
-        scale_coeffs.append(scalings.get(ch_type, 1))
-    vmin, vmax = _setup_vmin_vmax(data, vmin, vmax)
 
+    # make a copy because we discard non-data channels and scale the data
+    epochs = epochs.copy()
+    # use layout to subset channels present in epochs object
     if layout is None:
         layout = find_layout(epochs.info)
+    ch_names = set(layout.names) & set(epochs.ch_names)
+    idxs = [epochs.ch_names.index(ch_name) for ch_name in ch_names]
+    epochs = epochs.pick(idxs)
+    # iterate over a sequential index to get lists of chan. type & scale coef.
+    ch_idxs = range(epochs.info['nchan'])
+    ch_types = [channel_type(epochs.info, idx) for idx in ch_idxs]
+    scale_coeffs = [scalings.get(ch_type, 1) for ch_type in ch_types]
+    # scale the data
+    epochs._data *= np.array(scale_coeffs)[:, np.newaxis]
+    data = epochs.get_data()
+    # get vlims for each channel type
+    vlim_dict = dict()
+    for ch_type in set(ch_types):
+        this_data = data[:, np.where(np.array(ch_types) == ch_type)]
+        vlim_dict[ch_type] = _setup_vmin_vmax(this_data, vmin, vmax)
+    vlim_array = np.array([vlim_dict[ch_type] for ch_type in ch_types])
+    # only show colorbar if we have a single channel type
+    if colorbar is None:
+        colorbar = (len(set(ch_types)) == 1)
+    # if colorbar=True, we know we have only 1 channel type so all entries
+    # in vlim_array are the same, just take the first one
+    if colorbar and vmin is None and vmax is None:
+        vmin, vmax = vlim_array[0]
 
     show_func = partial(_erfimage_imshow_unified, scalings=scale_coeffs,
                         order=order, data=data, epochs=epochs, sigma=sigma,
-                        cmap=cmap)
+                        cmap=cmap, vlim_array=vlim_array)
+
     erf_imshow = partial(_erfimage_imshow, scalings=scale_coeffs, order=order,
-                         data=data, epochs=epochs, sigma=sigma, cmap=cmap)
+                         data=data, epochs=epochs, sigma=sigma, cmap=cmap,
+                         vlim_array=vlim_array, colorbar=True)
 
     fig = _plot_topo(info=epochs.info, times=epochs.times,
                      click_func=erf_imshow, show_func=show_func, layout=layout,


Test patch:
diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py
--- a/mne/viz/tests/test_topo.py
+++ b/mne/viz/tests/test_topo.py
@@ -192,6 +192,14 @@ def test_plot_topo_image_epochs():
     num_figures_before = len(plt.get_fignums())
     _fake_click(fig, fig.axes[0], (0.08, 0.64))
     assert num_figures_before + 1 == len(plt.get_fignums())
+    # test for auto-showing a colorbar when only 1 sensor type
+    ep = epochs.copy().pick_types(meg=False, eeg=True)
+    fig = plot_topo_image_epochs(ep, vmin=None, vmax=None, colorbar=None,
+                                 cmap=cmap)
+    ax = [x for x in fig.get_children() if isinstance(x, matplotlib.axes.Axes)]
+    qm_cmap = [y.cmap for x in ax for y in x.get_children()
+               if isinstance(y, matplotlib.collections.QuadMesh)]
+    assert qm_cmap[0] is cmap
     plt.close('all')