Repository: mne-tools/mne-python

Issue:
Custom colormap normalization in power plot
I'd like to create a classic ERD/ERS time/frequency plot by computing a TFR (e.g. via `tfr_multitaper`). Power should be normalized to a baseline with `mode='percent'`. I want to set the limits to `vmin=-1` and `vmax=1.5` using `cmap='RdBu'` (because these are the values typically used in ERD/ERS maps). This works fine, but now I'd like to map the value 0 to white; negative values should linearly scale from -1 (red) to 0 (white), and positive values should linearly scale from 0 (white) to 1.5 (blue).

I found [this example](https://matplotlib.org/users/colormapnorms.html#custom-normalization-two-linear-ranges) on how to normalize colormaps using the `norm` argument of `pcolormesh` (the relevant line in MNE that produces the plot seems to be [here](https://github.com/mne-tools/mne-python/blob/master/mne/viz/topo.py#L309)). Is it possible to do this after the plot has been created (if so, how)? Or is this only possible by specifying the `norm` argument for `pcolormesh`?


Discussion:
no idea. Never did this.

[This SO post](https://stackoverflow.com/a/20528097/1112283) describes how this could be done with a custom colormap - which would mean that no internal changes are necessary. However, I wonder if MNE should provide this out of the box to support creating standard ERD/ERS maps.
provide a gist by passing a custom cmap

It's pretty straightforward, but I feel that generating a standard ERDS map (as per the settings used in the Pfurtscheller papers) could be useful for some people. That's why I initially created [a small package](https://github.com/cbrnr/erds), but later realized that this should really be done by MNE (because it already has all the stuff).

Here's basically what needs to be done:
```{Python}
vmin, vmax = -1, 1.5
vzero = abs(vmin) / (vmax - vmin)
cmap = plt.cm.RdBu
index_old = np.linspace(0, 1, cmap.N)
index_new = np.hstack([np.linspace(0, vzero, cmap.N / 2, endpoint=False),
                       np.linspace(vzero, 1, cmap.N / 2)])
cdict = {"red": [], "green": [], "blue": [], "alpha": []}
for old, new in zip(index_old, index_new):
    r, g, b, a = cmap(old)
    cdict["red"].append((new, r, r))
    cdict["green"].append((new, g, g))
    cdict["blue"].append((new, b, b))
    cdict["alpha"].append((new, a, a))

newcmap = LinearSegmentedColormap("erds", cdict)

power.plot([0], baseline=[-2.5, -0.5], mode="percent", vmin=vmin, vmax=vmax,
           cmap=(newcmap, False))
```

Of course this could be factored out into a function. Would it make sense to add this to the examples so that people actually find it?
Creating a custom colormap seems a bit of a hack when IIUC `norm` should do exactly what you want.

Maybe we should have a `norm` argument that:

1. If `'centered'`, will use `vmin` and `vmax` and make `0` at the center of the colormap
2. If anything else, pass to `matplotlib` (default `None`)

And then say in the docstring that `centered` can be useful for (wacky) asymmetrical plots where you still want zero at the center of the colormap.
Yes, this would be really nice, but we'd still have to hack together a class that does the rescaling. I wish the Matplotlib devs implemented `colors.OffsetNorm` like they say on their website, this would completely solve this issue. Otherwise, if you think this is worth adding a new argument I'd be happy to implement it. If not, I'd argue for adding an example script on how to plot such ERDS maps.
They give code for how to do it with `MidpointNormalize`. Let's put it in `mne.fixes` under the assumption they'll add it eventually, then we're good to go with my API suggestion right?
my first reaction is make it an example with entry in the FAQ

To me it depends on how standard / often used it is, which I don't know

But it looks like you can modify it after the fact with `mesh.set_norm`, so we can go the example route pretty simply
I know a lot of people who produce such ERDS maps, so I think MNE should make this task as straightforward as possible. I kind of lean toward adding an example too, since knowing a lot of people is no indicator of how standard or how often it is used :smile:. I'll submit a PR. Am I correct in assuming that the example should go in [this section](https://martinos.org/mne/stable/auto_examples/index.html#time-frequency-examples)?
Yeah that's the one
yes


Resolving patch:
diff --git a/examples/time_frequency/plot_time_frequency_erds.py b/examples/time_frequency/plot_time_frequency_erds.py
new file mode 100644
--- /dev/null
+++ b/examples/time_frequency/plot_time_frequency_erds.py
@@ -0,0 +1,121 @@
+"""
+===============================
+Compute and visualize ERDS maps
+===============================
+
+This example calculates and displays ERDS maps of event-related EEG data. ERDS
+(sometimes also written as ERD/ERS) is short for event-related
+desynchronization (ERD) and event-related synchronization (ERS) [1]_.
+Conceptually, ERD corresponds to a decrease in power in a specific frequency
+band relative to a baseline. Similarly, ERS corresponds to an increase in
+power. An ERDS map is a time/frequency representation of ERD/ERS over a range
+of frequencies [2]_. ERDS maps are also known as ERSP (event-related spectral
+perturbation) [3]_.
+
+We use a public EEG BCI data set containing two different motor imagery tasks
+available at PhysioNet. The two tasks are imagined hand and feet movement. Our
+goal is to generate ERDS maps for each of the two tasks.
+
+First, we load the data and create epochs of 5s length. The data sets contain
+multiple channels, but we will only consider the three channels C3, Cz, and C4.
+We compute maps containing frequencies ranging from 2 to 35Hz. We map ERD to
+red color and ERS to blue color, which is the convention in many ERDS
+publications. Note that we do not perform any significance tests on the map
+values, but instead we display the whole time/frequency maps.
+
+References
+----------
+
+.. [1] G. Pfurtscheller, F. H. Lopes da Silva. Event-related EEG/MEG
+       synchronization and desynchronization: basic principles. Clinical
+       Neurophysiology 110(11), 1842-1857, 1999.
+.. [2] B. Graimann, J. E. Huggins, S. P. Levine, G. Pfurtscheller.
+       Visualization of significant ERD/ERS patterns in multichannel EEG and
+       ECoG data. Clinical Neurophysiology 113(1), 43-47, 2002.
+.. [3] S. Makeig. Auditory event-related dynamics of the EEG spectrum and
+       effects of exposure to tones. Electroencephalography and Clinical
+       Neurophysiology 86(4), 283-293, 1993.
+"""
+# Authors: Clemens Brunner <clemens.brunner@gmail.com>
+#
+# License: BSD (3-clause)
+
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.colors import LinearSegmentedColormap
+import mne
+from mne.datasets import eegbci
+from mne.io import concatenate_raws, read_raw_edf
+from mne.time_frequency import tfr_multitaper
+
+
+def center_cmap(cmap, vmin, vmax):
+    """Center given colormap (ranging from vmin to vmax) at value 0.
+
+    Note that eventually this could also be achieved by re-normalizing a given
+    colormap by subclassing matplotlib.colors.Normalize as described here:
+    https://matplotlib.org/users/colormapnorms.html#custom-normalization-two-linear-ranges
+    """  # noqa: E501
+    vzero = abs(vmin) / (vmax - vmin)
+    index_old = np.linspace(0, 1, cmap.N)
+    index_new = np.hstack([np.linspace(0, vzero, cmap.N // 2, endpoint=False),
+                           np.linspace(vzero, 1, cmap.N // 2)])
+    cdict = {"red": [], "green": [], "blue": [], "alpha": []}
+    for old, new in zip(index_old, index_new):
+        r, g, b, a = cmap(old)
+        cdict["red"].append((new, r, r))
+        cdict["green"].append((new, g, g))
+        cdict["blue"].append((new, b, b))
+        cdict["alpha"].append((new, a, a))
+    return LinearSegmentedColormap("erds", cdict)
+
+
+# load and preprocess data ####################################################
+subject = 1  # use data from subject 1
+runs = [6, 10, 14]  # use only hand and feet motor imagery runs
+
+fnames = eegbci.load_data(subject, runs)
+raws = [read_raw_edf(f, preload=True, stim_channel='auto') for f in fnames]
+raw = concatenate_raws(raws)
+
+raw.rename_channels(lambda x: x.strip('.'))  # remove dots from channel names
+
+events = mne.find_events(raw, shortest_event=0, stim_channel='STI 014')
+
+picks = mne.pick_channels(raw.info["ch_names"], ["C3", "Cz", "C4"])
+
+# epoch data ##################################################################
+tmin, tmax = -1, 4  # define epochs around events (in s)
+event_ids = dict(hands=2, feet=3)  # map event IDs to tasks
+
+epochs = mne.Epochs(raw, events, event_ids, tmin - 0.5, tmax + 0.5,
+                    picks=picks, baseline=None, preload=True)
+
+# compute ERDS maps ###########################################################
+freqs = np.arange(2, 36, 1)  # frequencies from 2-35Hz
+n_cycles = freqs  # use constant t/f resolution
+vmin, vmax = -1, 1.5  # set min and max ERDS values in plot
+cmap = center_cmap(plt.cm.RdBu, vmin, vmax)  # zero maps to white
+
+for event in event_ids:
+    power = tfr_multitaper(epochs[event], freqs=freqs, n_cycles=n_cycles,
+                           use_fft=True, return_itc=False, decim=2)
+    power.crop(tmin, tmax)
+
+    fig, ax = plt.subplots(1, 4, figsize=(12, 4),
+                           gridspec_kw={"width_ratios": [10, 10, 10, 1]})
+    for i in range(3):
+        power.plot([i], baseline=[-1, 0], mode="percent", vmin=vmin, vmax=vmax,
+                   cmap=(cmap, False), axes=ax[i], colorbar=False, show=False)
+        ax[i].set_title(epochs.ch_names[i], fontsize=10)
+        ax[i].set_xlabel("t (ms)")
+        ax[i].axvline(0, linewidth=1, color="black", linestyle=":")  # event
+        if i > 0:
+            ax[i].set_ylabel("")
+            ax[i].set_yticklabels("")
+        else:
+            ax[i].set_ylabel("f (Hz)")
+    fig.colorbar(ax[0].collections[0], cax=ax[-1])
+    fig.suptitle("ERDS ({})".format(event))
+    fig.show()


Test patch: