Search code examples
pythonmatplotliblogarithmcolormap

Matplotlib Symmetric Logarithmic colormap not centered at zero


I was looking at an issue where the default colormap, SymLogNorm imported from matplotlib.colors, isn't actually symmetric for my data. My data's mean and median are above zero, but I want values of zero to appear as the color white. SymLogNorm returns a colormap with center value very slightly above zero -- subtle, but definitely noticeable.

Is there any solution to this problem?


Solution

  • Here's my fix - it essentially uses a manual linear scale for the regions close to the center, and uses a logarithmic scale for the regions further outside. You specify lin_thres and the other arguments just as you would for SymLogNorm.

    class MidpointLogNorm(colors.SymLogNorm):
        """
        Normalise the colorbar so that diverging bars work there way either side from a prescribed midpoint value)
        e.g. im=ax1.imshow(array, norm=MidpointNormalize(midpoint=0.,vmin=-100, vmax=100))
    
        All arguments are the same as SymLogNorm, except for midpoint    
        """
        def __init__(self, lin_thres, lin_scale, midpoint=None, vmin=None, vmax=None):
            self.midpoint = midpoint
            self.lin_thres = lin_thres
            self.lin_scale = lin_scale
            #fraction of the cmap that the linear component occupies
            self.linear_proportion = (lin_scale / (lin_scale + 1)) * 0.5
            print(self.linear_proportion)
    
            colors.SymLogNorm.__init__(self, lin_thres, lin_scale, vmin, vmax)
    
        def __get_value__(self, v, log_val, clip=None):
            if v < -self.lin_thres or v > self.lin_thres:
                return log_val
            
            x = [-self.lin_thres, self.midpoint, self.lin_thres]
            y = [0.5 - self.linear_proportion, 0.5, 0.5 + self.linear_proportion]
            interpol = np.interp(v, x, y)
            return interpol
    
        def __call__(self, value, clip=None):
            log_val = colors.SymLogNorm.__call__(self, value)
    
            out = [0] * len(value)
            for i, v in enumerate(value):
                out[i] = self.__get_value__(v, log_val[i])
    
            return np.ma.masked_array(out)
    

    I drew inspiration centering around the midpoint from here: http://chris35wills.github.io/matplotlib_diverging_colorbar/