Repository: mne-tools/mne-python

Issue:
Selecting ICA components in `ica.plot_components` doesn't work anymore
If I click on the title of a component in the dialog produced by `ica.plot_components(inst=raw)`, I get the following error:

```
  File "/usr/local/lib/python3.6/site-packages/matplotlib/cbook/__init__.py", line 389, in process
    proxy(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/matplotlib/cbook/__init__.py", line 227, in __call__
    return mtd(*args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/mne/viz/topomap.py", line 1006, in onclick
    label = event.inaxes.get_label()
AttributeError: 'NoneType' object has no attribute 'get_label'
```

Any idea why the label is gone and how to retrieve it (actually, `event.inaxes` is `None`)?


Discussion:
I'll take a look in the evening, which matplotlib version do you have?
Thanks! I have the latest Matplotlib (2.1.0).
What is your expectation for what should happen when you click the title? I can replicate your problem but if you want to open component properties then click on the topo, not the title. Then this would be an easy fix.
Clicking on the title should mark/unmark a component as bad (i.e. include/exclude in `ica.exclude`).
Ah, right, that's something that you've added IIRC. Where is that set up in the code (I looked but couldn't find it)? Because the error comes from the piece of code that plots properties, not tries to change `ica.exclude`.
What, I added this? :open_mouth:
Correct me if I'm wrong. :) I looked into PR history and found this related PR: #4285, but it does only change title coloring, no interactivity is added. When was the title-related interactivity added?
AFAIR I wanted to add interactivity but then we decided to quickly merge this PR with only the color change implemented. So even if I did not yet add interactivity we still need to handle this error, right? We should be able to click on the title and nothing should happen.
Yes, that was why I mentioned that it should be an easy fix if clicking on titles doesn't have any intended functionality now. I will push a PR today. I can also add the selection by title clicking, unless you want to do it. :)
Actually, if you want to add the interactive functionality I'd be more than happy! :smile: 
Ok, I'll try to do it in one PR.
Because clicking on the title will always give you inaxes of None, the selection (changing state from excluded to included and the reverse) can be done in an easy or hard way:
* *easy* - right-clicking the topo changes IC state
* *harder* - clicking the title changes ICA state, but that would require checking the bounding boxes of titles with respect to xy pos of the mouse click.

Because of this I will split this into two PRs. First - small fix so that inaxes of None is ignored and only when we decide which of the above scenario we want I'll add it in another PR.
BTW anyone knows how to quickly find all the title text objects? In this case I can do it by inspecting all text object and seeing which have a text beginning with `'ICA'` but I was wondering if there is an easier way.
Once this is done we could check all text objects with their `contains` method against the mouse event.
Instead of inspecting all, we know there is only one we care about. So it might be easier to know which `Axes` instance you're interested in and check only the `contains` of its title text object as `ax.title` IIRC.
> Because clicking on the title will always give you inaxes of None

Did this change recently? AFAIR, you could click on the title and nothing happened.

From a UX perspective, the harder option (clicking on the title) would be preferable. In plot_raw, you can click on the y-axis channel labels to select/deselect a channel - is this something similar you could use?
(and if you don't know which, iterate over `fig.axes` and find the one with an appropriate `ax.get_title()` str)
@larsoner
Cool, I forgot about `ax.title` :) But this would sitll require testing all titles: if the user clicks on title I only know the x and y of the mouse. 
I'm thinking:
```
for ax in fig.axes:
    if ax.get_title().startswith('ICA '):
        # check ax.title
```
Yes, this is similar to my line of thinking :) But we can gather the titles beforehand - during the construction of ic topos. So I actually meant "all ic titles".

Resolving patch:
diff --git a/mne/viz/ica.py b/mne/viz/ica.py
--- a/mne/viz/ica.py
+++ b/mne/viz/ica.py
@@ -926,7 +926,7 @@ def _label_clicked(pos, params):
     if line_idx >= len(params['picks']):
         return
     ic_idx = [params['picks'][line_idx]]
-    if params['types'][ic_idx[0]] != 'misc':
+    if params['types'][line_idx] != 'misc':
         warn('Can only plot ICA components.')
         return
     types = list()
@@ -962,6 +962,6 @@ def _label_clicked(pos, params):
             plot_topomap(data_.flatten(), pos, axes=ax, show=False)
             _hide_frame(ax)
     tight_layout(fig=fig)
-    fig.subplots_adjust(top=0.95)
+    fig.subplots_adjust(top=0.88, bottom=0.)
     fig.canvas.draw()
     plt_show(True)
diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py
--- a/mne/viz/topomap.py
+++ b/mne/viz/topomap.py
@@ -916,6 +916,15 @@ def plot_ica_components(ica, picks=None, ch_type=None, res=64,
     -------
     fig : instance of matplotlib.pyplot.Figure or list
         The figure object(s).
+
+    Notes
+    -----
+    When run in interactive mode, ``plot_ica_components`` allows to reject
+    components by clicking on their title label. The state of each component
+    is indicated by its label color (gray: rejected; black: retained). It is
+    also possible to open component properties by clicking on the component
+    topomap (this option is only available when the ``inst`` argument is
+    supplied).
     """
     from ..io import BaseRaw
     from ..epochs import BaseEpochs
@@ -971,9 +980,10 @@ def plot_ica_components(ica, picks=None, ch_type=None, res=64,
 
     if merge_grads:
         from ..channels.layout import _merge_grad_data
+    titles = list()
     for ii, data_, ax in zip(picks, data, axes):
         kwargs = dict(color='gray') if ii in ica.exclude else dict()
-        ax.set_title(ica._ica_names[ii], fontsize=12, **kwargs)
+        titles.append(ax.set_title(ica._ica_names[ii], fontsize=12, **kwargs))
         data_ = _merge_grad_data(data_) if merge_grads else data_
         vmin_, vmax_ = _setup_vmin_vmax(data_, vmin, vmax)
         im = plot_topomap(data_.flatten(), pos, vmin=vmin_, vmax=vmax_,
@@ -993,16 +1003,41 @@ def plot_ica_components(ica, picks=None, ch_type=None, res=64,
                 ax.CB = DraggableColorbar(cbar, im)
         _hide_frame(ax)
     tight_layout(fig=fig)
-    fig.subplots_adjust(top=0.95)
+    fig.subplots_adjust(top=0.88, bottom=0.)
     fig.canvas.draw()
+
+    # add title selection interactivity
+    def onclick_title(event, ica=ica, titles=titles):
+        # check if any title was pressed
+        title_pressed = None
+        for title in titles:
+            if title.contains(event)[0]:
+                title_pressed = title
+                break
+        # title was pressed -> identify the IC
+        if title_pressed is not None:
+            label = title_pressed.get_text()
+            ic = int(label[-3:])
+            # add or remove IC from exclude depending on current state
+            if ic in ica.exclude:
+                ica.exclude.remove(ic)
+                title_pressed.set_color('k')
+            else:
+                ica.exclude.append(ic)
+                title_pressed.set_color('gray')
+            fig.canvas.draw()
+    fig.canvas.mpl_connect('button_press_event', onclick_title)
+
+    # add plot_properties interactivity only if inst was passed
     if isinstance(inst, (BaseRaw, BaseEpochs)):
-        def onclick(event, ica=ica, inst=inst):
+        def onclick_topo(event, ica=ica, inst=inst):
             # check which component to plot
-            label = event.inaxes.get_label()
-            if label.startswith('ICA'):
-                ic = int(label[-3:])
-                ica.plot_properties(inst, picks=ic, show=True)
-        fig.canvas.mpl_connect('button_press_event', onclick)
+            if event.inaxes is not None:
+                label = event.inaxes.get_label()
+                if label.startswith('ICA'):
+                    ic = int(label[-3:])
+                    ica.plot_properties(inst, picks=ic, show=True)
+        fig.canvas.mpl_connect('button_press_event', onclick_topo)
 
     plt_show(show)
     return fig
diff --git a/mne/viz/utils.py b/mne/viz/utils.py
--- a/mne/viz/utils.py
+++ b/mne/viz/utils.py
@@ -384,7 +384,8 @@ def _prepare_trellis(n_cells, max_col):
     else:
         nrow, ncol = int(math.ceil(n_cells / float(max_col))), max_col
 
-    fig, axes = plt.subplots(nrow, ncol, figsize=(7.4, 1.5 * nrow + 1))
+    fig, axes = plt.subplots(nrow, ncol, figsize=(1.3 * ncol + 1,
+                                                  1.5 * nrow + 1))
     axes = [axes] if ncol == nrow == 1 else axes.flatten()
     for ax in axes[n_cells:]:  # hide unused axes
         # XXX: Previously done by ax.set_visible(False), but because of mpl
@@ -1164,6 +1165,8 @@ def _fake_click(fig, ax, point, xform='ax', button=1, kind='press'):
         x, y = ax.transAxes.transform_point(point)
     elif xform == 'data':
         x, y = ax.transData.transform_point(point)
+    elif xform == 'pix':
+        x, y = point
     else:
         raise ValueError('unknown transform')
     if kind == 'press':


Test patch:
diff --git a/mne/viz/tests/test_ica.py b/mne/viz/tests/test_ica.py
--- a/mne/viz/tests/test_ica.py
+++ b/mne/viz/tests/test_ica.py
@@ -72,24 +72,37 @@ def test_plot_ica_components():
         for components in [0, [0], [0, 1], [0, 1] * 2, None]:
             ica.plot_components(components, image_interp='bilinear',
                                 colorbar=True, **fast_test)
+        plt.close('all')
 
         # test interactive mode (passing 'inst' arg)
-        plt.close('all')
         ica.plot_components([0, 1], image_interp='bilinear', inst=raw, res=16)
-
         fig = plt.gcf()
-        ax = [a for a in fig.get_children() if isinstance(a, plt.Axes)]
-        lbl = ax[1].get_label()
-        _fake_click(fig, ax[1], (0., 0.), xform='data')
+
+        # test title click
+        # ----------------
+        lbl = fig.axes[1].get_label()
+        ica_idx = int(lbl[-3:])
+        titles = [ax.title for ax in fig.axes]
+        title_pos_midpoint = (titles[1].get_window_extent().extents
+                              .reshape((2, 2)).mean(axis=0))
+        # first click adds to exclude
+        _fake_click(fig, fig.axes[1], title_pos_midpoint, xform='pix')
+        assert ica_idx in ica.exclude
+        # clicking again removes from exclude
+        _fake_click(fig, fig.axes[1], title_pos_midpoint, xform='pix')
+        assert ica_idx not in ica.exclude
+
+        # test topo click
+        # ---------------
+        _fake_click(fig, fig.axes[1], (0., 0.), xform='data')
 
         c_fig = plt.gcf()
-        ax = [a for a in c_fig.get_children() if isinstance(a, plt.Axes)]
-        labels = [a.get_label() for a in ax]
+        labels = [ax.get_label() for ax in c_fig.axes]
 
         for l in ['topomap', 'image', 'erp', 'spectrum', 'variance']:
             assert_true(l in labels)
 
-        topomap_ax = ax[labels.index('topomap')]
+        topomap_ax = c_fig.axes[labels.index('topomap')]
         title = topomap_ax.get_title()
         assert_true(lbl == title)
 
@@ -179,6 +192,11 @@ def test_plot_ica_sources():
     # Sadly close_event isn't called on Agg backend and the test always passes.
     assert_array_equal(ica.exclude, [1])
 
+    fig = ica.plot_sources(raw, [1])
+    # test mouse clicks
+    data_ax = fig.axes[0]
+    _fake_click(fig, data_ax, [-0.1, 0.9])  # click on y-label
+
     raw.info['bads'] = ['MEG 0113']
     assert_raises(RuntimeError, ica.plot_sources, inst=raw)
     ica.plot_sources(epochs)