mne.viz.plot_topomap - align data to head outline

  • MNE-Python version: 0.23.0
  • macOS


I am trying to plot a topomap of data that I calculated for each channel in some MEG data. So for example for channel: MEG0113 I may have a value of 0.5.

I am able to extract x,y positions for each channel using:

# Just gradiometers
layout = mne.channels.read_layout('Vectorview-grad')
channelPos = pd.DataFrame(layout.pos)
channelPos['channel'] = layout.names

which gives me a dataframe that looks like:

Screen Shot 2021-09-05 at 8.57.54 PM

I am then using the x and y positions to plot my data with the plot_topomap function like so:

# create a two-panel figure
fig,(ax1,ax2) = plt.subplots(ncols=2, figsize=[12,8])

im, cm = mne.viz.plot_topomap(data1, channelPos[['x', 'y']].values, axes=ax1, names=channelPos['channel'], show_names=False, vmin=vmin_, vmax=vmax_, contours=0, show=False, cmap='Greens')
im, cm = mne.viz.plot_topomap(data2, channelPos[['x', 'y']].values, axes=ax2, names=channelPos['channel'], show_names=False, vmin=vmin_, vmax=vmax_, contours=0, show=False, cmap='Greens')

# colorbar
ax_x_start = 0.95
ax_x_width = 0.02
ax_y_start = 0.3
ax_y_height = 0.5
cbar_ax = fig.add_axes([ax_x_start, ax_y_start, ax_x_width, ax_y_height])
clb = fig.colorbar(im, cax=cbar_ax)"units",fontsize=10)

gives the following plots:

I’m wondering (1) why the positions are not aligned to the center, and (2) why does the size not fit on the head outline?

I can manually change the x/y values in the channelPos dataframe to align it to head, but is there an automatic way to align it? Or should I be plotting the values in a different way entirely?

Thank you

Hello @user23 and welcome to the forum!

Im not very familiar with this specific plotting code. However, it does appear as if MNE would require you to conduct some sort of coordinate system transformation. If you imagine a 2D system with the origin in the center of the head outlines, then the elements you plotted are all located in the first quadrant. This obviously needs to be shifted as the first step, and then scaled.

@drammock is more knowledgeable on this than I am, maybe he can provide some more hints.

Best wishes,

Hi Richard, thank you for the reply.

Yes, I was hoping there would be a parameter for the plot_topomap function that would allow me to orient the coordinates, but didn’t see anything like that in the documentation. I think I’ll go with manually adjusting the x/y positions for now.


There are at least a couple problems here:

  1. The layout class docstring says layout.pos contains unit-normalized location values (all values between 0 and 1). That is not always true (e.g., if you do mne.channels.read_layout(..., scale=False). So the class docstring needs to be updated.

  2. The docstring of mne.channels.read_layout() says that scale=True (the default) applies “useful scaling for out the box plotting using layout.pos”… which is a debatable claim given the bad results we’re seeing here. I’m not really sure why we’re scaling to unit-normalized values; probabably to make it easier for downstream functions like plot_layout() or for plotting sensor insets in things like plot_joint().

To get this to work with mne.viz.plot_topomap(my_data, layout.pos[:, :2]), simply passing scale=False to read_layout isn’t enough though; nor is dividing the x,y columns of layout.pos by their max-minus-min, or by the values in… After ~15 minutes of poking around I can’t actually find an easy solution. To me this code looks ripe for a refactor. @mmagnuski or @larsoner do you have any ideas?

Hi @user23,
plot_topomap function assumes the channel positions are in head space. Also it usually deals with 3d positions and projects these to sphere (which is different from just ignoring the z dimension). The sphere (that is represented with head outline in the plot) origin and radius can be modified using the sphere argument. You can read more in this tutorial and this example.

@mmagnuski neither of those examples/tutorials show how to use plot_topomap with something other than an instance of Info, but according to its docstring it’s supposed to work with an array of x,y channel positions:

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.

So any tips on how to transform the unit-normalized layout x,y into head space? It seems like providing sphere=(x, y, z, radius) is going to require figuring out the inverse of that transformation anyway, in order to get the coords correct.

1 Like

Good point @drammock - I’m just not very familiar with MEG so I don’t know what coordinates are used and how people transform to head space in such case (I’m also not familiar with layout, for EEG you just use the montage).
For EEG, I frequently use this small function, which fits the head origin and radius to channel positions and then corrects channel positions so that the default mne sphere would produce good results. But many of the datasets I used do not contain digitized positions so it is ok do something like that.

I am trying to plot a topomap of data that I calculated for each channel in some MEG data. So for example for channel: MEG0113 I may have a value of 0.5.

Stepping back for a minute, why does evoked.plot_topomap itself automatically not just do the right thing for you? Personally I would find it much easier and robust to make an EvokedArray of your data with a suitable info and let things be taken care of automatically. I would not use layout at all for this stuff.

Also, the topomap interpretation for un-combined grad channels for VectorView is weird/difficult because each close channel pair will actually measure orthogonal directions. The native evoked.plot_topomap will do a grad pair norm, which helps you in that regard. (plot_topo, on the other hand, could be used if you really want to keep the two directions separate, since it plots tiny time courses rather than doing a color interpolation.)