mne.viz.plot_evoked_topomap and colorbar

I would like to add colorbars to the topoplots. Ideally, I would like to have only a single bar for all of them. But, this seems to mess up the scaling and the plots end up in different sizes.

When I try the code below and set colorbar = True. I get this error message:

You must provide 2 axes (one for each time plus one for the colorbar), got 1.

I understand that topo and colorbar are two subplots, but do not know how to fix it.

Maybe it is possible to draw a colorbar manually? Unfortunately, I cannot figure it out. Just get lots of errors.

vlim = [-1, 1]
n = 1
fig = plt.figure(figsize=(12, 4))
pi = [False, False, False]
for a, s, p in zip(axi, spi, pi):
    ax = fig.add_subplot(1,3,n) 
    # Plot topo
    mne.viz.plot_evoked_topomap(mne.grand_average(aevos[s]['diff']), 
                                show=False,
                                axes=ax,
                                vlim=vlim,
                                times=(ERP_time['AAN'][0]+ERP_time['AAN'][1])/2, average=0.105, 
                                show_names=False,
                                sensors=True,
                                mask=mAANch,
                                colorbar=p,
                                contours=False,
                                size=3)
    ax.set_title(s)
    n+=1
plt.suptitle(f'AAN ')
fig.supxlabel(f'Colors range from {vlim[0]} (blue) to {vlim[1]} \u03BCV (red)')
plt.savefig(os.path.join(DIRFILES, 'AAN_topo.png'), dpi=300)
plt.close()

the error message tells you the problem: if you pass specific axes and you also want a colorbar, you must pass a list/tuple of two axes (one for the data and one for the colorbar). If you don’t pass in axes the function should create the necessary axes for you.

Hi Dan!

Thanks for your advice. My problem is that I do not know enough python to revise the code accordingly. Can you help?

This part:

you are creating variable ax and then passing it to the plot_evoked_topomap function as axes=ax. Doing it that way won’t work unless you create 2 axes and pass in both of them. That is tricky to do because usually people want their colorbars to be much smaller than their data axes. You could try something like this:

data_ax = fig.add_axes([0.1, 0.1, 0.2, 0.8])  # left, bottom, width, height, in fraction of figure size
cbar_ax = fig.add_axes([0.3, 0.1, 0.05, 0.8])

and then in the call to plot_evoked_topomap pass the axes as axes=[data_ax, cbar_ax]

Since it looks like you’re looping to create 3 plots in one figure, you’ll need to offset the left value on each run of the loop too (you could add the offset as another variable in your zip() for example)

Great! Many thanks. I got hung up with the two axes. Never worked with those before.

The code below works fine (although positions need to be adjusted).

I get three subplots. The neat thing is that cbar is written three times but at the same location because cbar_ax is constant :). So, strictly speaking, the colorbar is correct only for the last subplot, but it looks good.

vlim = [-1, 1]
n = [1, 2, 3]
x = [0, 0.2, 0.4]
fig = plt.figure(figsize=(12, 4))
pi = [True, True, True]
for a, s, p, n, x in zip(axi, spi, pi, n, x):
  #ax = fig.add_subplot(1,3,n) #I want 4 rows and 2 columns
  data_ax = fig.add_axes([0.1+x, 0.1, 0.2, 0.8]) # left, bottom, width, height, in fraction of figure size
  cbar_ax = fig.add_axes([0.7, 0.1, 0.05, 0.8])
  # Plot topo
  mne.viz.plot_evoked_topomap(mne.grand_average(aevos[s]['diff']),
  show=False,
  axes=[data_ax, cbar_ax],
  vlim=vlim,
  times=(ERP_time['AAN'][0]+ERP_time['AAN'][1])/2, average=0.105,
  show_names=False,
  sensors=True,
  mask=mAANch,
  colorbar=p,
  contours=False,
  size=3)
  ax.set_title(s)
  #plt.suptitle(f'AAN ')
  #fig.supxlabel(f'Colors range from {vlim[0]} (blue) to {vlim[1]} \u03BCV (red)')
plt.show()

FYI there is also an option vlim="joint" that will make sure that the colormap has limits that are appropriate for all of the subplots. That only works if you’re plotting multiple time spans from a single evoked object though — which I don’t think you are doing? Hard to be sure when there are extra looping variables axi, n that look like they aren’t getting used and variables axi spi that aren’t defined in your example code.

I would recommend simplifying the loop as much as possible (e.g., don’t include axi if you’re never going to use a; don’t loop over pi if its value is always going to be True
). But even then it looks like you’re using different evokeds as the input (aevos[s]['diff'] where s is changing each time) so maybe the vlim='joint' approach won’t work.

Here is a clean solution according to the suggestions.

However, one final thing that I cannot fix: Each call to plot_evoked_topomap creates new axes and the plot has a title with the interval. It would be nice if I could change this directly. I tried to use this command:

fig.get_axes()[0].set_title(“test”)

That changed the title only of the first subplot.

Best
Stefan

vlim = [-3, 3]
sall = [('digit', 'detect'), ('digit', 'identify'),
     ('letter', 'detect'), ('letter', 'identify')]
xall = [0.05, 0.43, 0.05, 0.43]
yall = [0.9, 0.9, 0.43, 0.43]
fig = plt.figure(figsize=(7, 6))
for s, x, y in zip(sall, xall, yall):
    plt.text(x+0.02, y, f'{s[0]}-{s[1]}', fontsize=14)
plt.axis('off')
xall = [0.05, 0.4, 0.05, 0.4]
yall = [0.50, 0.50, 0.05, 0.05]
for s, x, y in zip(sall, xall, yall):
    data_ax = fig.add_axes([x, y, 0.30, 0.30]) # left, bottom, width, height, in fraction of figure size
    cbar_ax = fig.add_axes([0.85, 0.1, 0.05, 0.8])
    # Plot topo
    mne.viz.plot_evoked_topomap(mne.grand_average(all_VAN_evoked[s[0]][s[1]]['diff']),
        show=False,
        axes=[data_ax, cbar_ax],
        vlim=vlim,
        times=(VANtime[0]+VANtime[1])/2, 
        average=0.105,
        show_names=False,
        sensors=True,
        mask=maskcht,
        colorbar=True,
        contours=False,
        size=3)
    # fig.get_axes()[0].set_title("test")
fig.suptitle(f'VAN: Aware minus unaware topoplot')
#plt.show()
plt.savefig(os.path.join(DIRFILES, 'figures', 'VAN_topoplot.png'), dpi=300)
plt.close()

1 Like

the [0] selects the first axes, so only the first axes is changed.

for ax in fig.axes:
    ax.set_title("test")

(note: this might affect the colorbar too, which you might not want. you could, e.g., detect if the title is ÎŒV and skip the set_title in that case)

1 Like

Nice idea, many thanks.

Alas, it does not work. I tried it as part of the for s, x, y loop and also outside.

TypeError: ‘list’ object is not callable

I was inspired by your suggestion and tried some more. This worked outside of the loop:

for ax in fig.get_axes():
if ax.get_title() != “”V”:
lbl = sall.pop(0)
ax.set_title(f’{lbl[0]}-{lbl[1]}')

This worked! Preferably, one would like to change the title right after the mne plot_evoked. This would minimze potential mismatch errors. I could not figure out how to do that though.

I think you need to change

to

for ax in fig.axes:

Indeed, that solved it.

If it is not too much of a hazzle, is it possible to change the titles right after each mne plot_evoked call? Would minimize errors.

oops, yes, I got confused between fig.axes (no parentheses) and fig.get_axes() (with parentheses). I’ve corrected the earlier post.

within your for-loop, keep track of the return value of plot_evoked_topomap (here img and cont) and use the first return value to access the axes:

    img, cont = mne.viz.plot_evoked_topomap(
        mne.grand_average(all_VAN_evoked[s[0]][s[1]]['diff']),
        show=False, axes=[data_ax, cbar_ax], vlim=vlim,
        times=(VANtime[0]+VANtime[1])/2, average=0.105, show_names=False, sensors=True,
        mask=maskcht, colorbar=True, contours=False, size=3)
    img.axes.set_title("whatever")
1 Like

That sounds nice, but I get an error at img, cont =mne.viz.

TypeError: cannot unpack non-iterable Figure object

Change this:

to

img = mne.viz.plot_evoked_topomap(

when I run this with

img.axes.set_title(“whatever”)

I get an error

AttributeError: ‘list’ object has no attribute ‘set_title’

I think the problem is that img is updated in each loop.

When I tried this.

for ax in img.axes:
print(s, ax.get_title())

Output shows that ‘ax’ is updated in each loop and increases in size. This makes it difficult to access only the “current” axis.

I thought that img.axes[0].set_title(“whatever”) would work, but this updates only the first subplot.

You can update the titles inside this for loop:

Yes, that works, but it requires one to keep track of which axis is what. I asked a colleague, and he had an interesting fix.

The img is adding new axes on every iteration. The command changes the title for the current iteration.

img.axes[-1].set_title(f'{s[0]}-{s[1]}')

I am attaching the whole script below. I struggled with this plotting question for a long time. I hope this will be helpful to others.

Many thanks for your generous help, guys! :kissing_heart:

# Plot VAN topos
# =============
# Expand mask channels in time
# (needed for all times)
maskcht = mVANch.reshape(len(mVANch), 1)
maskcht = numpy.matlib.repmat(maskcht, 1, len(all_times))
vlim = [-3, 3]
sall = [('digit', 'detect'), ('digit', 'identify'),
     ('letter', 'detect'), ('letter', 'identify')]
fig = plt.figure(figsize=(7, 6))
# for s, x, y in zip(sall, xall, yall):
#     plt.text(x+0.02, y, f'{s[0]}-{s[1]}', fontsize=14)
# plt.axis('off')
xall = [0.05, 0.4, 0.05, 0.4]
yall = [0.50, 0.50, 0.05, 0.05]
cbar_ax = fig.add_axes([0.85, 0.1, 0.05, 0.8])
for s, x, y in zip(sall, xall, yall):
    data_ax = fig.add_axes([x, y, 0.4, 0.4]) # left, bottom, width, height, in fraction of figure size
    # Plot topo
    img = mne.viz.plot_evoked_topomap(mne.grand_average(all_VAN_evoked[s[0]][s[1]]['diff']),
        show=False,
        axes=[data_ax, cbar_ax],
        vlim=vlim,
        times=(VANtime[0]+VANtime[1])/2, 
        average=0.105,
        show_names=False,
        sensors=True,
        mask=maskcht,
        colorbar=True,
        contours=False,
        size=3)
    img.axes[-1].set_title(f'{s[0]}-{s[1]}')
    # for ax in img.axes:
    #     print(s, ax.get_title())
fig.suptitle(f'VAN: Aware minus unaware topoplots')
#plt.show()
plt.savefig(os.path.join(DIRFILES, 'figures', 'VAN_topoplots.png'), dpi=300)
plt.close()
1 Like