Repository: mne-tools/mne-python

Issue:
Much too hard to plot an array as a topomap?
There isn't a simple one-liner to plot an array the shape of your sensors as a topomap right? Something like:

`mne.viz._plot_topomap_for_dummies(array, info)`

(Assuming `info` has as many data channels as array has values.)

You always have to mess with layouts etc. This is easier in EEGLAB, which can plot based on just an array and chanlocs.

IME it makes quite a bit of code much harder than need be, both internally and for users.

Or am I missing a function/usage?



Discussion:
I'd be fine with a new

plot_array_topomap

as I often also end up using an EvokedArray just for this and times are not
always meaningful

We don't necessarily need a new function. The difficulty mostly stems from `plot_topomap` not being able to take coordinates in 3D. It requires coordinates already mapped to 2D (actually, layout might not even properly do this, as there is an offset of box_width/2 and box_height/2). The best method is to use the private function `_find_topomap_coords`. We could make that function public. Even more convenient would be to make `plot_topomap` detect when coordinates are given in 3D (`if coords.ndim == 3`) and call the `_find_topomap_coords` function.

@dengemann, care to weigh in?

I'd still like to see a function that works off of info and an array. 

Sorry having difficulties to connect. Isn't auto layout taking care of everything? Not sure we need a new API. I think we can still improve our default behavior, I have the suspicion that @jona-sassenhagen is saying that something is not user friendly ;)

> as I often also end up using an EvokedArray just for this and times are not
> always meaningful

Ah I see. Yes me too. @jona-sassenhagen is this your point?

I just think it should be possible to plot an array without any boilerplate, such as extra function calls, mock objects etc. All the information is in info, after all. 

Ok agreed. 

please share a code snippet of how you would like to write stuff.

I'm also +1 on this. 

> please share a code snippet of how you would like to write stuff.

You mean API?

The easiest solution would be to allow mne.viz.plot_topomap to take an info instance (instead of only a pos array). Then, instead of doing

```
    picks, pos, merge_grads, _, ch_type = _prepare_topo_plot(evoked,
                                                             ch_type,
                                                             layout=None)
    plot_topomap(arr, pos)
```

which IMO is not very beginner friendly and requires accessing private functions, one could do

`plot_topomap(arr, info)`

@agramfort what do you think?

I don't know the details but I like a

plot_array_topomap(arr, info)

just make it work without breaking the API and duplicating code :)

Adding a param for plot_topomap is cleaner than a new function, don't you think?

I would say no if you make params redundant or conflicting (one param
shadows another)

+1 for new function!

> On 23 Apr 2016, at 17:06, Alexandre Gramfort notifications@github.com wrote:
> 
> I would say no if you make params redundant or conflicting (one param
> shadows another)
> —
> You are receiving this because you were mentioned.
> Reply to this email directly or view it on GitHub

Hm ... plot_array_topomap doesn't seem like a good name because plot_topomap already plots an array as a topomap. In fact, the two functions should be 99% identical, the only difference being that plot_topomap has pos as its second argument, and plot_array_topomap an info.

In fact, as @wmvanvliet points out, it would be roughly equivalent to

`plot_topomap(array, _find_topomap_coords(info))`

How about overloading the pos keyword for plot_topomap so it also allows an info object?

@wmvanvliet s suggestion of making _find_topomap would partially ameliorate the problem, but we probably also have to deal with merging grads.

Okay, this is my last objection, if you're still not convinced, I'm starting work on plot_array_topomap :)

On Sat, Apr 23, 2016 at 5:24 PM, jona-sassenhagen notifications@github.com
wrote:

> In fact, as @wmvanvliet https://github.com/wmvanvliet points out, it
> would be roughly equivalent to
> 
> plot_topomap(array, _find_topomap_coords(info))
> 
> if this really works I am fine with overloading the pos param

It sounds like it probably is easy enough to do `if isinstance(pos, Info): ...`, so +1 on that for me. Then it's just a few lines of code to extract the relevant positions (probably de-duplicated with where it's already done elsewhere) and a docstring update.

@jona-sassenhagen if you can do it in the next couple of days it can go in 0.12

I will try, please assign me Eric :) But make it .12, .13.


Resolving patch:
diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py
--- a/mne/viz/topomap.py
+++ b/mne/viz/topomap.py
@@ -18,7 +18,8 @@
 
 from ..baseline import rescale
 from ..io.constants import FIFF
-from ..io.pick import pick_types, _picks_by_type
+from ..io.pick import (pick_types, _picks_by_type, channel_type, pick_info,
+                       _pick_data_channels)
 from ..utils import _clean_names, _time_mask, verbose, logger, warn
 from .utils import (tight_layout, _setup_vmin_vmax, _prepare_trellis,
                     _check_delayed_ssp, _draw_proj_checkbox, figure_nobar,
@@ -26,11 +27,12 @@
 from ..time_frequency import psd_multitaper
 from ..defaults import _handle_default
 from ..channels.layout import _find_topomap_coords
+from ..io.meas_info import Info
 
 
 def _prepare_topo_plot(inst, ch_type, layout):
     """"Aux Function"""
-    info = copy.deepcopy(inst.info)
+    info = copy.deepcopy(inst if isinstance(inst, Info) else inst.info)
 
     if layout is None and ch_type is not 'eeg':
         from ..channels import find_layout
@@ -384,10 +386,14 @@ def plot_topomap(data, pos, vmin=None, vmax=None, cmap=None, sensors=True,
 
     Parameters
     ----------
-    data : array, length = n_points
+    data : array, shape (n_chan,)
         The data values to plot.
-    pos : array, shape = (n_points, 2)
-        For each data point, the x and y coordinates.
+    pos : array, shape (n_chan, 2) | instance of Info
+        Location information for the data points(/channels).
+        If an array, for each data point, the x and y coordinates.
+        If an Info object, it must contain only one data type and
+        exactly `len(data)` data channels, and the x/y coordinates will
+        be inferred from this Info object.
     vmin : float | callable | None
         The value specifying the lower bound of the color range.
         If None, and vmax is None, -vmax is used. Else np.min(data).
@@ -468,6 +474,35 @@ def plot_topomap(data, pos, vmin=None, vmax=None, cmap=None, sensors=True,
     from matplotlib.widgets import RectangleSelector
 
     data = np.asarray(data)
+
+    if isinstance(pos, Info):  # infer pos from Info object
+        picks = _pick_data_channels(pos)  # pick only data channels
+        pos = pick_info(pos, picks)
+
+        # check if there is only 1 channel type, and n_chans matches the data
+        ch_type = set(channel_type(pos, idx)
+                      for idx, _ in enumerate(pos["chs"]))
+        info_help = ("Pick Info with e.g. mne.pick_info and "
+                     "mne.channels.channel_indices_by_type.")
+        if len(ch_type) > 1:
+            raise ValueError("Multiple channel types in Info structure. " +
+                             info_help)
+        elif len(pos["chs"]) != data.shape[0]:
+            raise ValueError("Number of channels in the Info object and "
+                             "the data array does not match. " + info_help)
+        else:
+            ch_type = ch_type.pop()
+
+        if any(type_ in ch_type for type_ in ('planar', 'grad')):
+            # deal with grad pairs
+            from ..channels.layout import (_merge_grad_data, find_layout,
+                                           _pair_grad_sensors)
+            picks, pos = _pair_grad_sensors(pos, find_layout(pos))
+            data = _merge_grad_data(data[picks]).reshape(-1)
+        else:
+            picks = list(range(data.shape[0]))
+            pos = _find_topomap_coords(pos, picks=picks)
+
     if data.ndim > 1:
         raise ValueError("Data needs to be array of shape (n_sensors,); got "
                          "shape %s." % str(data.shape))


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
@@ -60,6 +60,7 @@ def test_plot_topomap():
     res = 16
     evoked = read_evokeds(evoked_fname, 'Left Auditory',
                           baseline=(None, 0))
+
     # Test animation
     _, anim = evoked.animate_topomap(ch_type='grad', times=[0, 0.1],
                                      butterfly=False)
@@ -109,6 +110,13 @@ def test_plot_topomap():
                     for x in subplot.get_children()
                     if isinstance(x, matplotlib.text.Text)))
 
+    # Plot array
+    for ch_type in ('mag', 'grad'):
+        evoked_ = evoked.copy().pick_types(eeg=False, meg=ch_type)
+        plot_topomap(evoked_.data[:, 0], evoked_.info)
+    # fail with multiple channel types
+    assert_raises(ValueError, plot_topomap, evoked.data[0, :], evoked.info)
+
     # Test title
     def get_texts(p):
         return [x.get_text() for x in p.get_children() if