Repository: mne-tools/mne-python

Issue:
applying projectors to CTF data (BUG?)
I'm seeing strange behavior applying EOG and ECG projectors to CTF datasets.

with epochs created with `proj='delayed' I'm looking at the effect of the projection operators using the following code.
```
times = 1e-3 * np.array([86, 148, 94, 160])
epochs.average().plot_topomap(times, title='No Proj', proj=False,
                              vmin=-100, vmax=100, show=False)
epochs.average().plot_topomap(times, title='Proj', proj=True,
                              vmin=-100, vmax=100, show=False)
epochs.average().plot_topomap(times, title='Delayed Proj',
                              proj='interactive',
                              vmin=-100, vmax=100, show=True)
```

I have 2 issues. 
1) with all the checkboxes checked the topoplots don't match the maps from `proj=True`, I like the interactive plots better.
2) the check boxes aren't reversible. if I click all of them and then unclick them all I don't end up where I started.

you can find screen shots here the show this 
https://www.dropbox.com/sh/dqdirsnre6hqma8/AAByY4MuxUbBozcKsvFAJ4Qya?dl=0

apply_proj_nothingChecked.png : the delayed plot matched the No Proj plot
apply_proj_allChecked.png: the delayed plot does not match the Proj plot. 
apply_proj_nothingChecked2.png: this is the delayed plot with nothing checked after selecting everything. it does not match the No Proj plot. 

Am I just doing something wrong? The code for applying the projs interactively (https://github.com/mne-tools/mne-python/blob/master/mne/viz/topomap.py#L100-L104) seems like it should match `proj=True` case and be fully reversible.  So i'm abit baffled. Does any one have any ideas?







Discussion:
> Am I just doing something wrong? The code for applying the projs interactively (https://github.com/mne-tools/mne-python/blob/master/mne/viz/topomap.py#L100-L104) seems like it should match proj=True case and be fully reversible.

Yes this is supposed to be the behavior. We must have introduced a regression somewhere that is causing this. I could try to address this in:

https://github.com/mne-tools/mne-python/pull/4972

As it aims to unify `raw` and `epochs` plotting, which is two places where this can happen. But there are probably more. It's probably worth doing a separate, more manageable PR to fix only the bug(s) reported here.
@bloyl can you see if this behaves as you'd expect:
```
import mne
data_path = mne.datasets.sample.data_path()
raw = mne.io.read_raw_fif(data_path + '/MEG/sample/sample_audvis_raw.fif')
raw.load_data().pick_types(meg='mag', stim=True)
events = mne.find_events(raw, 'STI 014')
epochs = mne.Epochs(raw, events, proj='delayed')
evoked = epochs.average()
kwargs = dict(vmin=-100, vmax=100)
evoked.plot_topomap(proj=False, title='No proj', **kwargs)
evoked.plot_topomap(proj=True, title='Proj', **kwargs)
evoked.plot_topomap(proj='interactive', title='Interactive', **kwargs)
```
If so, then it could be a problem with CTF compensation channels or something.

If not, then I'll be confused because it seems to work fine over here.
This doesn't seem quite right in 0.15.2, did you want me to be testing it in PR #4972 ?

https://www.dropbox.com/s/nx8r0ww5ew0ivtx/topos_withProj.png?dl=0
here I clicked all 3 proj boxes. 

At 266ms the left sensors appear less red then in the other plots, maybe the vmin/vmax settings aren't being maintained so the colormapping is off?
Also no matter what is clicked the contours don't update.



Just latest `master` is fine. That's more or less where I was testing it.

Looking closer I agree, the contours at least are broken / don't update. I'll try to look into it soon to get it into 0.16.

Resolving patch:
diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py
--- a/mne/viz/topomap.py
+++ b/mne/viz/topomap.py
@@ -13,7 +13,6 @@
 from functools import partial
 import itertools
 from numbers import Integral
-import warnings
 
 import numpy as np
 
@@ -107,21 +106,29 @@ def _plot_update_evoked_topomap(params, bools):
     data = new_evoked.data[:, params['time_idx']] * params['scale']
     if params['merge_grads']:
         data = _merge_grad_data(data)
-    image_off = ~params['image_mask']
 
-    pos = np.asarray(params['pos'])[:, :2]
-    pos_x, pos_y = pos.T
-    interp = _GridData(pos)
-
-    xi = np.linspace(pos_x.min(), pos_x.max(), params['res'])
-    yi = np.linspace(pos_y.min(), pos_y.max(), params['res'])
-    Xi, Yi = np.meshgrid(xi, yi)
-    for ii, im in enumerate(params['images']):
-        Zi = interp.set_values(data[:, ii])(Xi, Yi)
-        Zi[image_off] = np.nan
+    interp = params['interp']
+    new_contours = list()
+    for cont, ax, im, d in zip(params['contours_'], params['axes'],
+                               params['images'], data.T):
+        Zi = interp.set_values(d)()
         im.set_data(Zi)
-    for cont in params['contours']:
-        cont.set_array(np.c_[Xi, Yi, Zi])
+        # must be removed and re-added
+        if len(cont.collections) > 0:
+            tp = cont.collections[0]
+            visible = tp.get_visible()
+            patch_ = tp.get_clip_path()
+            color = tp.get_color()
+            lw = tp.get_linewidth()
+        for tp in cont.collections:
+            tp.remove()
+        cont = ax.contour(interp.Xi, interp.Yi, Zi, params['contours'],
+                          colors=color, linewidths=lw)
+        for tp in cont.collections:
+            tp.set_visible(visible)
+            tp.set_clip_path(patch_)
+        new_contours.append(cont)
+    params['contours_'] = new_contours
 
     params['fig'].canvas.draw()
 
@@ -445,8 +452,16 @@ def set_values(self, v):
         self.interpolator = CloughTocher2DInterpolator(self.tri, v)
         return self
 
+    def set_locations(self, Xi, Yi):
+        """Set locations for easier (delayed) calling."""
+        self.Xi = Xi
+        self.Yi = Yi
+        return self
+
     def __call__(self, *args):
         """Evaluate the interpolator."""
+        if len(args) == 0:
+            args = [self.Xi, self.Yi]
         return self.interpolator(*args)
 
 
@@ -555,6 +570,17 @@ def plot_topomap(data, pos, vmin=None, vmax=None, cmap=None, sensors=True,
     cn : matplotlib.contour.ContourSet
         The fieldlines.
     """
+    return _plot_topomap(data, pos, vmin, vmax, cmap, sensors, res, axes,
+                         names, show_names, mask, mask_params, outlines,
+                         image_mask, contours, image_interp, show,
+                         head_pos, onselect)[:2]
+
+
+def _plot_topomap(data, pos, vmin=None, vmax=None, cmap=None, sensors=True,
+                  res=64, axes=None, names=None, show_names=False, mask=None,
+                  mask_params=None, outlines='head', image_mask=None,
+                  contours=6, image_interp='bilinear', show=True,
+                  head_pos=None, onselect=None):
     import matplotlib.pyplot as plt
     from matplotlib.widgets import RectangleSelector
     data = np.asarray(data)
@@ -641,7 +667,8 @@ def plot_topomap(data, pos, vmin=None, vmax=None, cmap=None, sensors=True,
     xi = np.linspace(xmin, xmax, res)
     yi = np.linspace(ymin, ymax, res)
     Xi, Yi = np.meshgrid(xi, yi)
-    Zi = _GridData(np.array((pos_x, pos_y)).T).set_values(data)(Xi, Yi)
+    interp = _GridData(np.array((pos_x, pos_y)).T).set_values(data)
+    Zi = interp.set_locations(Xi, Yi)()
 
     if outlines is None:
         _is_default_outlines = False
@@ -736,7 +763,7 @@ def _show_names(x):
     if onselect is not None:
         ax.RS = RectangleSelector(ax, onselect=onselect)
     plt_show(show)
-    return im, cont
+    return im, cont, interp
 
 
 def _make_image_mask(outlines, pos, res):
@@ -1434,8 +1461,7 @@ def plot_evoked_topomap(evoked, times="auto", ch_type=None, layout=None,
     # Skip comps check here by using private function
     evoked = evoked.copy()._pick_drop_channels(picks, check_comps=False)
 
-    with warnings.catch_warnings(record=True):  # elementwise comparison
-        interactive = times == 'interactive'
+    interactive = isinstance(times, string_types) and times == 'interactive'
     if axes is not None:
         if isinstance(axes, plt.Axes):
             axes = [axes]
@@ -1549,9 +1575,9 @@ def plot_evoked_topomap(evoked, times="auto", ch_type=None, layout=None,
                   outlines=outlines, image_mask=image_mask, contours=contours,
                   image_interp=image_interp, show=False)
     for idx, time in enumerate(times):
-        tp, cn = plot_topomap(data[:, idx], pos, axes=axes[idx],
-                              mask=mask_[:, idx] if mask is not None else None,
-                              **kwargs)
+        tp, cn, interp = _plot_topomap(
+            data[:, idx], pos, axes=axes[idx],
+            mask=mask_[:, idx] if mask is not None else None, **kwargs)
 
         images.append(tp)
         if cn is not None:
@@ -1594,12 +1620,12 @@ def plot_evoked_topomap(evoked, times="auto", ch_type=None, layout=None,
 
     if proj == 'interactive':
         _check_delayed_ssp(evoked)
-        params = dict(evoked=evoked, fig=fig, projs=evoked.info['projs'],
-                      picks=picks, images=images, contours=contours_,
-                      time_idx=time_idx, merge_grads=merge_grads,
-                      res=res, pos=pos, image_mask=image_mask,
-                      plot_update_proj_callback=_plot_update_evoked_topomap,
-                      scale=scaling)
+        params = dict(
+            evoked=evoked, fig=fig, projs=evoked.info['projs'], picks=picks,
+            images=images, contours_=contours_, pos=pos, time_idx=time_idx,
+            res=res, plot_update_proj_callback=_plot_update_evoked_topomap,
+            merge_grads=merge_grads, scale=scaling, axes=axes,
+            contours=contours, interp=interp)
         _draw_proj_checkbox(None, params)
 
     plt_show(show)


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
@@ -15,7 +15,8 @@
 from nose.tools import assert_true, assert_equal
 import pytest
 
-from mne import read_evokeds, read_proj, make_fixed_length_events, Epochs
+from mne import (read_evokeds, read_proj, make_fixed_length_events, Epochs,
+                 compute_proj_evoked)
 from mne.io.proj import make_eeg_average_ref_proj
 from mne.io import read_raw_fif, read_info
 from mne.io.constants import FIFF
@@ -52,6 +53,60 @@
 layout = read_layout('Vectorview-all')
 
 
+def test_plot_topomap_interactive():
+    """Test interactive topomap projection plotting."""
+    import matplotlib.pyplot as plt
+    from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
+    from matplotlib.figure import Figure
+    evoked = read_evokeds(evoked_fname, baseline=(None, 0))[0]
+    evoked.pick_types(meg='mag')
+    evoked.info['projs'] = []
+    assert not evoked.proj
+    evoked.add_proj(compute_proj_evoked(evoked, n_mag=1))
+
+    plt.close('all')
+    fig = Figure()
+    canvas = FigureCanvas(fig)
+    ax = fig.gca()
+
+    kwargs = dict(vmin=-240, vmax=240, times=[0.1], colorbar=False, axes=ax,
+                  res=8)
+    evoked.copy().plot_topomap(proj=False, **kwargs)
+    canvas.draw()
+    image_noproj = np.frombuffer(canvas.tostring_rgb(), dtype='uint8')
+    assert len(plt.get_fignums()) == 1
+
+    ax.clear()
+    evoked.copy().plot_topomap(proj=True, **kwargs)
+    canvas.draw()
+    image_proj = np.frombuffer(canvas.tostring_rgb(), dtype='uint8')
+    assert not np.array_equal(image_noproj, image_proj)
+    assert len(plt.get_fignums()) == 1
+
+    ax.clear()
+    evoked.copy().plot_topomap(proj='interactive', **kwargs)
+    canvas.draw()
+    image_interactive = np.frombuffer(canvas.tostring_rgb(), dtype='uint8')
+    assert_array_equal(image_noproj, image_interactive)
+    assert not np.array_equal(image_proj, image_interactive)
+    assert len(plt.get_fignums()) == 2
+
+    proj_fig = plt.figure(plt.get_fignums()[-1])
+    _fake_click(proj_fig, proj_fig.axes[0], [0.5, 0.5], xform='data')
+    canvas.draw()
+    image_interactive_click = np.frombuffer(
+        canvas.tostring_rgb(), dtype='uint8')
+    assert_array_equal(image_proj, image_interactive_click)
+    assert not np.array_equal(image_noproj, image_interactive_click)
+
+    _fake_click(proj_fig, proj_fig.axes[0], [0.5, 0.5], xform='data')
+    canvas.draw()
+    image_interactive_click = np.frombuffer(
+        canvas.tostring_rgb(), dtype='uint8')
+    assert_array_equal(image_noproj, image_interactive_click)
+    assert not np.array_equal(image_proj, image_interactive_click)
+
+
 @testing.requires_testing_data
 def test_plot_projs_topomap():
     """Test plot_projs_topomap."""