Tom Smith
Tom Smith

Reputation: 191

Shifted logarithmic colorbar, white color offset to center

I am struggling to plot my data using pyplot.imshow. I use the 'RdBu_r' colormap, and I need the white color to be at value 1 on a logarithmic scale which is not centered at 1. But how can I do it? I tried 'center=1' which works for seaborn, but there is no such attribute in matplotlib. I also tried this:

import matplotlib.pyplot as plt
im=plt.imshow(proportion, cmap="RdBu_r", norm=LogNorm(), vmin=0.01, vmax=10)
axs=plt.gca()
cb = plt.colorbar(im, ax=axs,extend="both")

where proportion is my data array, ranging from 0.01 to 10. However there seems to be no way to specify that the white should be at 1 on this scale. Is there a way to do that?

Note again that I need to make use a gradient of colors here and a logarithmic normalization.

Upvotes: 1

Views: 2479

Answers (2)

Jared
Jared

Reputation: 98

Take the logarithm of your data before plotting it, then use CenteredNorm or TwoSlopeNorm, followed by using a FuncFormatter on the colorbar axis to undo your logarithm. It’s maybe a bit less elegant than the other answer but I think easier to understand and maintain, and doesn’t suffer from not being able to handle edge cases like NaNs because it uses built in Norms.

import matplotlib.pyplot as plt
from matplotlib.colors import CenteredNorm, TwoSlopeNorm
import numpy as np

# Example data
x = y = np.linspace(0, 1, 21)
var = np.power(10, 2*(np.sin(10 * x)[:, np.newaxis] + np.cos(10 * y)[np.newaxis, :]) + 1)

# Transform data
base = 10
logvar = np.log(var)/np.log(base)

# list of centers at whole powers of the base
centers = [0, 1, 2]

# Plot data
fig, axes = plt.subplots(nrows=3, figsize=(10, 10), tight_layout=True)

for ax, center in zip(axes.flat, centers):

    im = ax.pcolormesh(y, x, logvar, cmap='bwr', norm=TwoSlopeNorm(vcenter=center))
    # For a symmetric colour scale with asymmetric data, use CenteredNorm

    # Set the title
    ax.set_title(f'Center on power: {center}')
    
    # Add a colorbar
    cbar = plt.colorbar(im, ax=ax)
    # Place ticks at whole powers of the base.
    cbar.ax.yaxis.set_major_locator(plt.MultipleLocator(1))
    # Define your custom tick formatter function to undo the log transformation
    cbar.ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, pos: f'${base:d}^{{{x:.0f}}}$'))

plt.show()

enter image description here

Upvotes: 1

ImportanceOfBeingErnest
ImportanceOfBeingErnest

Reputation: 339520

There are some questions and answers about defining a midpoint on a colorscale. Especially this one, which is also now part of the matplotlib documentation.

The idea is to subclass matplotlib.colors.Normalize and let it take a further argument midpoint. This can then be used to linearly interpolate the two ranges on either side of the midpoint to the ranges [0,0.5] and [0.5,1].

To have a midpoint on a logarithmic scale, we can in principle do the same thing, just that we subclass matplotlib.colors.LogNorm and take the logarithm of all values, then interpolate this logarithm on the ranges [0,0.5] and [0.5,1].

In the following example we have data between 0.001 and 10. Using the usual LogNorm this results in the middle of the colormap (white in the case of the RdBu colormap) to be at 0.1. If we want to have white at 1, we specify 1 as the midpoint in the MidPointLogNorm.

import numpy as np
import matplotlib.pyplot as plt
from  matplotlib.colors import LogNorm

x,y = np.meshgrid(np.linspace(-3,0,19), np.arange(10))
f = lambda x,y : 10**x*(1+y)
z = f(x,y)

fig, (ax,ax2) = plt.subplots(ncols=2, figsize=(12,4.8))

im = ax.pcolormesh(x,y,z, cmap="RdBu_r", norm=LogNorm(vmin=z.min(), vmax=z.max()))
fig.colorbar(im, ax=ax)
ax.set_title("LogNorm")

class MidPointLogNorm(LogNorm):
    def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
        LogNorm.__init__(self,vmin=vmin, vmax=vmax, clip=clip)
        self.midpoint=midpoint
    def __call__(self, value, clip=None):
        # I'm ignoring masked values and all kinds of edge cases to make a
        # simple example...
        x, y = [np.log(self.vmin), np.log(self.midpoint), np.log(self.vmax)], [0, 0.5, 1]
        return np.ma.masked_array(np.interp(np.log(value), x, y))


im2 = ax2.pcolormesh(x,y,z, cmap="RdBu_r", 
                            norm=MidPointLogNorm(vmin=z.min(), vmax=z.max(), midpoint=1))
fig.colorbar(im2, ax=ax2)
ax2.set_title("MidPointLogNorm")
plt.show()

enter image description here


Updated solution which works for nan values: You need to replace the nan values by some value (best one outside the range of values from the array) then mask the array by those numbers. Inside the MidPointLogNorm we need to take care of nan values, as shown in this question.

import numpy as np
import matplotlib.pyplot as plt
from  matplotlib.colors import LogNorm

x,y = np.meshgrid(np.linspace(-3,0,19), np.arange(10))
f = lambda x,y : 10**x*(1+y)
z = f(x,y)
z[1:3,1:3] = np.NaN

#since nan values cannot be used on a log scale, we need to change them to 
# something other than nan, 
replace = np.nanmax(z)+900
z = np.where(np.isnan(z), replace, z)
# now we can mask the array
z = np.ma.masked_where(z == replace, z)

fig, (ax,ax2) = plt.subplots(ncols=2, figsize=(12,4.8))

im = ax.pcolormesh(x,y,z, cmap="RdBu_r", norm=LogNorm(vmin=z.min(), vmax=z.max()))
fig.colorbar(im, ax=ax)
ax.set_title("LogNorm")

class MidPointLogNorm(LogNorm):
    def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
        LogNorm.__init__(self,vmin=vmin, vmax=vmax, clip=clip)
        self.midpoint=midpoint
    def __call__(self, value, clip=None):
        result, is_scalar = self.process_value(value)
        x, y = [np.log(self.vmin), np.log(self.midpoint), np.log(self.vmax)], [0, 0.5, 1]
        return np.ma.array(np.interp(np.log(value), x, y), mask=result.mask, copy=False)


im2 = ax2.pcolormesh(x,y,z, cmap="RdBu_r", 
                            norm=MidPointLogNorm(vmin=z.min(), vmax=z.max(), midpoint=1))
fig.colorbar(im2, ax=ax2)
ax2.set_title("MidPointLogNorm")
plt.show()

Upvotes: 2

Related Questions