Repository: mne-tools/mne-python

Issue:
raw.plot() silently applies projectors that are disabled
**how it works now**

- inactive projectors are automatically applied during `raw.plot()`
- a small "proj" button appears in the bottom right of the plot window
- Clicking the proj button shows a list of projectors, each with a large "X" next to it.  

**how it should work**

- if a raw object has projectors that are ***not activated***, `raw.plot()` should plot the ***untransformed data***
- the big "X" marks should be replaced with check marks, or something else that is less ambiguous (maybe click on the projector names to make them black vs. grey, analogous to bad channels?).  It is quite plausible to interpret "X" as either "selected" (i.e., "active") or as "crossed off" (i.e., "not active").
- at a minimum, the whole phenomenon of the "proj" button should be documented in the `raw.plot()` docstring.
- optionally, `raw.plot()` could get a `proj` argument that lets the projectors get applied (non-interactively) before plotting.  The simplest version might accept only "all or none" booleans; a more complex implementation might accept a list of indices into `raw.projs` indicating which projections to activate.



Discussion:
"active" means "have been applied to the data". But it's a bit of a misnomer as when you do source localization, both active and inactive end up being used/applied to the data.

> if a raw object has projectors that are not activated, raw.plot() should plot the untransformed data

For backward compat (and less importantly, least surprise for mne_browse converts) I don't think we should change this. The standard MNE pipeline will eventually apply them (`proj=True` in several places) so there is some rationale for it.

But +1 for visual clarifications, doc improvements, and even a `proj=True (default) | False` option. 
yeah, I know what "active" means.  That's not the problem.  There is advice in the docs that says (I'm paraphrasing):

> avoid applying projectors too early; use `<obj>.copy().apply_proj().plot()` to see what they're doing without altering the original data.  

The (undocumented) interactive "proj" button makes that advice moot (you don't need to make a copy first in order to see what changes a projector is making to the signal).  Even more perversely, if you do follow the advice, it doesn't actually work: both plots look the same, i.e., you get identical plots from `raw` and `raw2`:
```
raw.add_proj(blink_proj)  # or whatever other projector you want
raw2 = raw.copy()
raw2.apply_proj()
raw.plot()
raw2.plot()
```
If preserving backward compatibility is that important in this case, I think having an explicit `proj=True` as part of the function signature is necessary, to draw attention to the fact that this is happening by default.
I'd be fine with

raw.plot(proj=False)

option

@drammock do you have time to make a `proj=True` default PR?
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

I have a presentation late this afternoon, planning to start on it
tomorrow.

On 04/18/2017 06:04 AM, Eric Larson wrote:
> @drammock do you have time to make a
> |proj=True| default PR?
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> ,
> or mute the thread
> .
>

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2

iQEcBAEBCAAGBQJY9jDkAAoJEEzlbMQqUVLONtcH/3eP6/L86AM1Gp6sye2IkIn/
yp0fR2O6DTAsPa4cdXIeBI1YH8xCV8KIDA9dvKWsIFbUEh1aq1NHGCh/gourjbz3
K7k+oRsfEp3S5IG3BN6+Qn8D2DReykFpTsr61d31/fqQwf5BuD5XYb1STVcXgoP0
jErMcj9IF4zdJEZbQTW7Sgr93x6FRjx7hvLHcY04FzxcMz5C84irlUow3qb/Bl28
+I3vsZ/pPurWXxhD/IOtOktwFeJiElLiKHc4oRrWAZ7m068+H2Mk84P88bu5hk3K
/x/+nVg2+Vke6lkMZvOyKjUe8MuwASvqYfB/xB7qkoZqrtlNxZFNA1UhVC8+5XQ=
=mCZs
-----END PGP SIGNATURE-----


Resolving patch:
diff --git a/mne/io/base.py b/mne/io/base.py
--- a/mne/io/base.py
+++ b/mne/io/base.py
@@ -1725,11 +1725,11 @@ def plot(self, events=None, duration=10.0, start=0.0, n_channels=20,
              event_color='cyan', scalings=None, remove_dc=True, order='type',
              show_options=False, title=None, show=True, block=False,
              highpass=None, lowpass=None, filtorder=4, clipping=None,
-             show_first_samp=False):
+             show_first_samp=False, proj=True):
         return plot_raw(self, events, duration, start, n_channels, bgcolor,
                         color, bad_color, event_color, scalings, remove_dc,
                         order, show_options, title, show, block, highpass,
-                        lowpass, filtorder, clipping, show_first_samp)
+                        lowpass, filtorder, clipping, show_first_samp, proj)
 
     @verbose
     @copy_function_doc_to_method_doc(plot_raw_psd)
diff --git a/mne/viz/raw.py b/mne/viz/raw.py
--- a/mne/viz/raw.py
+++ b/mne/viz/raw.py
@@ -93,7 +93,7 @@ def plot_raw(raw, events=None, duration=10.0, start=0.0, n_channels=20,
              event_color='cyan', scalings=None, remove_dc=True, order='type',
              show_options=False, title=None, show=True, block=False,
              highpass=None, lowpass=None, filtorder=4, clipping=None,
-             show_first_samp=False):
+             show_first_samp=False, proj=True):
     """Plot raw data.
 
     Parameters
@@ -178,6 +178,11 @@ def plot_raw(raw, events=None, duration=10.0, start=0.0, n_channels=20,
         then excessive values are not shown, creating gaps in the traces.
     show_first_samp : bool
         If True, show time axis relative to the ``raw.first_samp``.
+    proj : bool
+        Whether to apply projectors prior to plotting (default is ``True``).
+        Individual projectors can be enabled/disabled interactively (see
+        Notes). This argument only affects the plot; use ``raw.apply_proj()``
+        to modify the data stored in the Raw object.
 
     Returns
     -------
@@ -194,6 +199,12 @@ def plot_raw(raw, events=None, duration=10.0, start=0.0, n_channels=20,
     can be to toggled with f11 key. To mark or un-mark a channel as bad, click
     on the rather flat segments of a channel's time series. The changes will be
     reflected immediately in the raw object's ``raw.info['bads']`` entry.
+
+    If projectors are present, a button labelled "Proj" in the lower right
+    corner of the plot window opens a secondary control window, which allows
+    enabling/disabling specific projectors individually. This provides a means
+    of interactively observing how each projector would affect the raw data if
+    it were applied.
     """
     import matplotlib.pyplot as plt
     import matplotlib as mpl
@@ -344,6 +355,7 @@ def plot_raw(raw, events=None, duration=10.0, start=0.0, n_channels=20,
     if len(raw.info['projs']) > 0 and not raw.proj:
         ax_button = plt.subplot2grid((10, 10), (9, 9))
         params['ax_button'] = ax_button
+        params['apply_proj'] = proj
         opt_button = mpl.widgets.Button(ax_button, 'Proj')
         callback_option = partial(_toggle_options, params=params)
         opt_button.on_clicked(callback_option)
@@ -372,7 +384,7 @@ def plot_raw(raw, events=None, duration=10.0, start=0.0, n_channels=20,
     _layout_figure(params)
 
     # deal with projectors
-    if show_options is True:
+    if show_options:
         _toggle_options(None, params)
 
     callback_close = partial(_close_event, params=params)
diff --git a/mne/viz/utils.py b/mne/viz/utils.py
--- a/mne/viz/utils.py
+++ b/mne/viz/utils.py
@@ -252,7 +252,8 @@ def _toggle_proj(event, params):
             if not b and p['active']:
                 bools[bi] = True
     else:
-        bools = [True] * len(params['projs'])
+        proj = params.get('apply_proj', True)
+        bools = [proj] * len(params['projs'])
 
     compute_proj = False
     if 'proj_bools' not in params:
@@ -385,19 +386,24 @@ def _draw_proj_checkbox(event, params, draw_current_state=True):
 
     labels = [p['desc'] for p in projs]
     actives = ([p['active'] for p in projs] if draw_current_state else
-               [True] * len(params['projs']))
+               [params['apply_proj']] * len(projs))
 
-    width = max([len(p['desc']) for p in projs]) / 6.0 + 0.5
-    height = len(projs) / 6.0 + 0.5
+    width = max([4., max([len(p['desc']) for p in projs]) / 6.0 + 0.5])
+    height = len(projs) / 6.0 + 1.5
     fig_proj = figure_nobar(figsize=(width, height))
     fig_proj.canvas.set_window_title('SSP projection vectors')
     params['fig_proj'] = fig_proj  # necessary for proper toggling
-    ax_temp = fig_proj.add_axes((0, 0, 1, 1), frameon=False)
+    ax_temp = fig_proj.add_axes((0, 0, 1, 0.8), frameon=False)
+    ax_temp.set_title('Projectors marked with "X" are active')
 
     proj_checks = widgets.CheckButtons(ax_temp, labels=labels, actives=actives)
+    # make edges around checkbox areas
+    [rect.set_edgecolor('0.5') for rect in proj_checks.rectangles]
+    [rect.set_linewidth(1.) for rect in proj_checks.rectangles]
+
     # change already-applied projectors to red
     for ii, p in enumerate(projs):
-        if p['active'] is True:
+        if p['active']:
             for x in proj_checks.lines[ii]:
                 x.set_color('r')
     # make minimal size


Test patch: