Matplotlib Symmetric Logarithmic colormap not centered at zero

Viewed 435

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?

2 Answers

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/

Your previous answer did not really work for me as the midpoint was "far" from the original midpoint. The issue is that midpoint is not taken into account in your lagorithmic values.

I changed your code a bit and fixed that issue by creating 2 SymLogNorm. Both are centered on midpoint. One starts from vmin and the other one ends at vmax (the extremas are filled according to midpoint).

If I take a value v, I assign the colorbar coordinates this way:

  • vmin < v < midpoint - lin_thresh, so use the SymLogNorm which starts from vmin

  • midpoint - lin_thresh < v < midpoint + lin_thresh, so use interpolation

  • midpoint + lin_thresh < v < vmax, so use the SymLogNorm which ends at vmax

    class MidpointLogNorm(colors.SymLogNorm):
      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)
    
          # Create norm with vmin at 0 and midpoint at 0.5
          self.SymLogNorm1 = colors.SymLogNorm(lin_thres, lin_scale, vmin, 2*self.midpoint + np.abs(vmin))
          # Create norm with midpoint at 0.5 and vmax at 1
          self.SymLogNorm2 = colors.SymLogNorm(lin_thres, lin_scale, 2*self.midpoint - vmax, vmax)
          colors.SymLogNorm.__init__(self, lin_thres, lin_scale, vmin, vmax)
    
      def __get_value__(self, v, log_val1_i, log_val2_i, clip=None):
          v = np.array(v)
          x = [self.vmin, self.midpoint, self.vmax]
          y = [0., 0.5, 1.]
          interpol = np.interp(v, x, y)
          out = np.where(np.abs(v) < self.lin_thres, interpol, v)
          out = np.where(out > self.lin_thres, log_val2_i, out)
          out = np.where(out < self.lin_thres, log_val1_i, out)
          return np.ma.masked_array(out)
    
      def __call__(self, value, clip=None):
          log_val1 = self.SymLogNorm1(value)
          log_val2 = self.SymLogNorm2(value)
    
          out = [0] * len(value)
          for i, v in enumerate(value):
              out[i] = self.__get_value__(v, log_val1[i], log_val2[i])
          return np.ma.masked_array(out)
    
Related