1

I'm trying to change a colorbar attached to a scatter plot so that the minimum and maximum of the colorbar are the minimum and maximum of the data, but I want the data to be centred at zero as I'm using a colormap with white at zero. Here is my example

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 1, 61)
y = np.linspace(0, 1, 61)
C = np.linspace(-10, 50, 61)
M = np.abs(C).max() # used for vmin and vmax

fig, ax = plt.subplots(1, 1, figsize=(5,3), dpi=150)
sc=ax.scatter(x, y, c=C, marker='o', edgecolor='k', vmin=-M, vmax=M, cmap=plt.cm.RdBu_r)
cbar=fig.colorbar(sc, ax=ax, label='$R - R_0$ (mm)')
ax.set_xlabel('x')
ax.set_ylabel('y')

enter image description here

As you can see from the attached figure, the colorbar goes down to -M, where as I want the bar to just go down to -10, but if I let vmin=-10 then the colorbar won't be zerod at white. Normally, setting vmin to +/- M when using contourf the colorbar automatically sorts to how I want. This sort of behaviour is what I expect when contourf uses levels=np.linspace(-M,M,61) rather than setting it with vmin and vmax with levels=62. An example showing the default contourf colorbar behaviour I want in my scatter example is shown below

plt.figure(figsize=(6,5), dpi=150)
plt.contourf(x, x, np.reshape(np.linspace(-10, 50, 61*61), (61,61)),
                   levels=62, vmin=-M, vmax=M, cmap=plt.cm.RdBu_r)
plt.colorbar(label='$R - R_0$ (mm)')

enter image description here

Does anyone have any thoughts? I found this link which I thought might solve the problem, but when executing the cbar.outline.set_ydata line I get this error AttributeError: 'Polygon' object has no attribute 'set_ydata' .

EDIT a little annoyed that someone has closed this question without allowing me to clarify any questions they might have, as none of the proposed solutions are what I'm asking for. As for Normalize.TwoSlopeNorm, I do not want to rescale the smaller negative side to use the entire colormap range, I just want the colorbar attached to the side of my graph to stop at -10. This link also does not solve my issue, as it's the TwoSlopeNorm solution again.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Steven Thomas
  • 352
  • 2
  • 9
  • `norm=TwoSlopeNorm(vmin=C.min(), vmax = C.max(), vcenter=0)` and `ax.scatter(...., norm=norm)` – JohanC Sep 20 '21 at 16:18
  • 1
    @JohanC No this doesn't solve the issue, as the blue (negative) part of the colorbar uses the whole range of half the colorbar, I don't want this. I had tried this but I couldn't remember the name of the function when writing my post. I will edit the post to reflect this, but I thought the comment about how contourf handles the colorbar was descriptive enough. I shall edit that too. – Steven Thomas Sep 20 '21 at 16:34
  • 2
    Just to explain why this works for `contourf` and not other artists. `contourf` colorbars just show the contours, and in this case you only have contours from -7 to 49. Other artists, the colorbar goes from `vmin` to `vmax`. What you want to do _should_ be possible in upcoming v3.5 of Matplotlib, but be aware that this doesn't have a lot of support in the Matplotlib dev group, and instead they wish to make the limits exactly track vmin and vmax. – Jody Klymak Sep 21 '21 at 07:26
  • @JodyKlymak Excellent explanation, thank you. It's weird that the colorbar acts differently across different functions in my opinion, but then I don't maintain things or work with the backend so I'm sure there is reason to the madness. – Steven Thomas Sep 21 '21 at 09:29

2 Answers2

4

After changing the ylim of the colorbar, the rectangle formed by the surrounding spines is too large. You can make this outline invisible. And then add a new rectangular border:

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 1, 61)
y = np.linspace(0, 1, 61)
C = np.linspace(-10, 50, 61)
M = np.abs(C).max()  # used for vmin and vmax

fig, ax = plt.subplots(1, 1, figsize=(5, 3), dpi=150)
sc = ax.scatter(x, y, c=C, marker='o', edgecolor='k', vmin=-M, vmax=M, cmap=plt.cm.RdBu_r)
cbar = fig.colorbar(sc, ax=ax, label='$R - R_0$ (mm)')

cb_ymin = C.min()
cb_ymax = C.max()
cb_xmin, cb_xmax = cbar.ax.get_xlim()
cbar.ax.set_ylim(cb_ymin, cb_ymax)
cbar.outline.set_visible(False)  # hide the surrounding spines, which are too large after set_ylim
cbar.ax.add_patch(plt.Rectangle((cb_xmin, cb_ymin), cb_xmax - cb_xmin, cb_ymax - cb_ymin,
                                fc='none', ec='black', clip_on=False))
plt.show()

reducing the colorbar

JohanC
  • 71,591
  • 8
  • 33
  • 66
  • 1
    Just a note that for v3.5 you should not need to delete the Spines any longer. – Jody Klymak Sep 20 '21 at 19:28
  • This works, however the box drawn around the colorbar doesn't line up quite as nicely as its own lines did, so I don't think I'm going to use this method. The first post I linked to in the original question pointed to something similar to this, to which the answer said it is quite a hacky way of doing it. Thank you anyway. – Steven Thomas Sep 21 '21 at 09:38
2

Another approach until v3.5 is released is to make a custom colormap that does what you want (see also https://matplotlib.org/stable/tutorials/colors/colormap-manipulation.html#sphx-glr-tutorials-colors-colormap-manipulation-py)

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm
from matplotlib.colors import ListedColormap

fig, axs = plt.subplots(2, 1)

X = np.random.randn(32, 32) + 2
pc = axs[0].pcolormesh(X, vmin=-6, vmax=6, cmap='RdBu_r')
fig.colorbar(pc, ax=axs[0])

import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm
from matplotlib.colors import ListedColormap

fig, axs = plt.subplots(2, 1)

X = np.random.randn(32, 32) + 2
pc = axs[0].pcolormesh(X, vmin=-6, vmax=6, cmap='RdBu_r')
fig.colorbar(pc, ax=axs[0])

def keep_center_colormap(vmin, vmax, center=0):
    vmin = vmin - center
    vmax = vmax - center
    dv = max(-vmin, vmax) * 2
    N = int(256 * dv / (vmax-vmin))
    RdBu_r = cm.get_cmap('RdBu_r', N)
    newcolors = RdBu_r(np.linspace(0, 1, N))
    beg = int((dv / 2 + vmin)*N / dv)
    end = N - int((dv / 2 - vmax)*N / dv)
    newmap = ListedColormap(newcolors[beg:end])
    return newmap

newmap = keep_center_colormap(-2, 6, center=0)
pc = axs[1].pcolormesh(X, vmin=-2, vmax=6, cmap=newmap)
fig.colorbar(pc, ax=axs[1])
plt.show()

enter image description here

Jody Klymak
  • 4,979
  • 2
  • 15
  • 31
  • Thanks for your answer, would you be able to ellaborate on the significance of the numbers you've used in your answer? Specifically, the ```256 * 3 / 2``` for ```N``` and the ```N / 3``` used for ```newmap```? – Steven Thomas Sep 21 '21 at 08:19
  • I have played around with this a little now and I now understand at least why the ```N / 3``` was chosen in this instance. My case of wanting to go from -10 to 50 is a little more difficult, as ```4 / 10``` of ```N = 256 * 3 / 2``` = 384 gives me 230.4. Is there any significance to the way you scaled the 256? Can I choose N to be anything? – Steven Thomas Sep 21 '21 at 09:40
  • 1
    Yes N can be anything, though you probably want it to be relatively modest. Sorry, I probably could have encapsulated this better... – Jody Klymak Sep 21 '21 at 10:46
  • Thanks, I wasn't sure if you'd picked it to be 1.5*256 based on some knowledge of the bit depth of the colormap or something like that. – Steven Thomas Sep 21 '21 at 10:47
  • 1
    To get exactly 256 values, you could also do `newmap = ListedColormap(RdBu_r(np.linspace(1/3, 1, 256)))`. – JohanC Sep 21 '21 at 11:02
  • 1
    I've updated with a slightly more general example, though @JohanC's suggestion above might be a good improvement. I'm always in favour of just using a big enough N that I don't notice if there are 256 or 255 elements, but that is sloppy. – Jody Klymak Sep 21 '21 at 11:08
  • 1
    ```vmin``` and ```vmax``` are wrong in your updated example, they should be -2 and 6, respectively. You have them as -6 and 2. The example is good though. As for the number of points to have in the colormap, this is an interesting discussion. Are they designed with 256 in mind? I tend to choose the number of levels/N based on the range of my values and what represents the data well, which I suppose is as good a reason as any for colormaps such as RdBu, but for something like viridis just used to show a range of values, I don't know what would be a good N. – Steven Thomas Sep 21 '21 at 12:13
  • N=256 is just Matplotlib's default. (sorry, fixed limits - I was testing the other case) – Jody Klymak Sep 21 '21 at 12:34