Reputation: 1371
I'm trying to plot an asymmetric color range in a scatter plot. I want the colors to be a fair representation of the intensity using a diverging color map. I am having trouble changing the color bar to represent this.
For instance, I want to plot x-y data with range [-2, 10] in a scatter plot such that the color bar only shows the range -2 to 10 with neutral color at 0, but the 'intensity' at -2 and 2 are the same.
I've tried using ColorMap Normalization and truncating the color map, but it seems I need some combination of the two that I can't figure out.
MCV Example
x = np.arange( 0, 1, 1e-1 )
xlen = x.shape[ 0 ]
z = np.random.random( xlen**2 )*12 - 2
splt = plt.scatter(
np.repeat( x, xlen ),
np.tile( x, xlen ),
c = z, cmap = 'seismic',
s = 400
)
plt.colorbar( splt )
By using the MidpointNormalize
class MidpointNormalize(colors.Normalize):
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
self.midpoint = midpoint
colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
# I'm ignoring masked values and all kinds of edge cases to make a
# simple example...
x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1]
return np.ma.masked_array(np.interp(value, x, y))
x = np.arange( 0, 1, 1e-1 )
xlen = x.shape[ 0 ]
z = np.random.random( xlen**2 )*12 - 2
norm = MidpointNormalize( midpoint = 0 )
splt = plt.scatter(
np.repeat( x, xlen ),
np.tile( x, xlen ),
c = z, cmap = 'seismic', s = 400,
norm = norm
)
plt.colorbar( splt )
I can get the colorbar centered at 0, but the intensities are unfair. i.e. The intensity at -2 is much darker that at +2.
The problem I've been having with truncating the color map, is I don't know where the fair place to truncate it is.
Here is an example of the change I want to make in the color bar:
Upvotes: 5
Views: 3200
Reputation: 1371
Based on @Asmus's answer I created a MidpointNormalizeFair
class that does this scaling based on the data.
class MidpointNormalizeFair(mpl.colors.Normalize):
""" From: https://matplotlib.org/users/colormapnorms.html"""
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
self.midpoint = midpoint
mpl.colors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
# I'm ignoring masked values and all kinds of edge cases to make a
# simple example...
result, is_scalar = self.process_value(value)
self.autoscale_None(result)
vlargest = max( abs( self.vmax - self.midpoint ), abs( self.vmin - self.midpoint ) )
x, y = [ self.midpoint - vlargest, self.midpoint, self.midpoint + vlargest], [0, 0.5, 1]
return np.ma.masked_array(np.interp(value, x, y))
Upvotes: 1
Reputation: 5247
If I get you correctly, the issue at hand is that your midpoint-centered map is scaling the color evenly from -2 to 0 (blue) and similarly (red) from 0 to 10.
Instead of scaling [self.vmin, self.midpoint, self.vmax] = [-2, 0, 10]
, you should rather rescale between [-v_ext, self.midpoint, v_ext] = [-10, 0, 10]
where:
v_ext = np.max( [ np.abs(self.vmin), np.abs(self.vmax) ] ) ## = np.max( [ 2, 10 ] )
The complete code could look like:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
x = np.arange( 0, 1, 1e-1 )
xlen = x.shape[ 0 ]
z = np.random.random( xlen**2 )*12 - 2
class MidpointNormalize(mcolors.Normalize):
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
self.midpoint = midpoint
mcolors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
v_ext = np.max( [ np.abs(self.vmin), np.abs(self.vmax) ] )
x, y = [-v_ext, self.midpoint, v_ext], [0, 0.5, 1]
return np.ma.masked_array(np.interp(value, x, y))
x = np.arange( 0, 1, 1e-1 )
xlen = x.shape[ 0 ]
z = np.random.random( xlen**2 )*12 - 2
norm = MidpointNormalize( midpoint = 0 )
splt = plt.scatter(
np.repeat( x, xlen ),
np.tile( x, xlen ),
c = z, cmap = 'seismic', s = 400,
norm = norm
)
plt.colorbar( splt )
plt.show()
Upvotes: 7