Search code examples
pythonmatplotlibcolorbarcontourf

Forcing a symmetrical color bar range on asymmetrical data for tricontorf


I currently wish to make a tricontorf plot for a large 2D dataset

import matplotlib.pyplot as plt
import numpy as np

# Load the 3D data file
data = np.genfromtxt("data.txt", skip_header=14, delimiter="\t", dtype = float)
reflect = data[:,0]
emiss = data[:,1]
tempdiff = data[:,4]
    
fig, ax = plt.subplots()
cb = ax.tricontourf(reflect, emiss, tempdiff, 200, vmin = -800, vmax = 800, cmap = "seismic")
cbar = plt.colorbar(cb)
cbar.set_label(r'z', rotation = 270, labelpad = 13)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_xlim([0.6,1])
ax.set_ylim([0,0.4])

plt.savefig('plot.pdf', bbox_inches='tight', format='pdf')
plt.savefig('plot.png', dpi=300, bbox_inches='tight', format='png')

plt.show()

Produced Plot

However, we can see that there is a large negative tail to z as the dataset has some large negative values in z. I wish to use a divergent color map so the white line in the "Seismic" color map lies at z=0. Is there a way to force a symmetric color bar from -800 to 800 without this large tail we see in the image?

I have tried using the vmin and vmax options in tricontorf but this only scales the range of the color map itself rather than the color bar.

Data file can be found here: https://drive.google.com/file/d/1ydPkAwtYkq7xeQxdw3t5obTAkXsdCrIS/view?usp=sharing


Solution

  • I played around with this problem for a while but wasn't able to get the desired results using the normal methods. So, I resorted to using the function from this answer, but I made the following modifications:

    1. Added fig and ax as optional arguments. If they aren't provided, then it will automatically find them. For tricontourf, it seems to need CS.axes instead of CS.ax that was in the original answer.
    2. Use kwargs.get("extend"), which returns None if "extend" is not a key. This avoids the verbose check for "extend" being a key and then checking the value.

    So, the code now becomes:

    import matplotlib.pyplot as plt
    import numpy as np
    
    plt.close("all")
    
    reflect, emiss, _, _, tempdiff = np.genfromtxt("data.txt",
                                                   skip_header=14,
                                                   delimiter="\t",
                                                   dtype=float,
                                                   unpack=True)
    
    
    def clippedcolorbar(CS, fig=None, ax=None, **kwargs):
        """https://stackoverflow.com/a/55403314/12131013"""
        from matplotlib.cm import ScalarMappable
        from numpy import arange, floor, ceil
    
        if ax is None:
            ax = CS.axes
        if fig is None:
            fig = ax.get_figure()
    
        vmin = CS.get_clim()[0]
        vmax = CS.get_clim()[1]
        m = ScalarMappable(cmap=CS.get_cmap())
        m.set_array(CS.get_array())
        m.set_clim(CS.get_clim())
        step = CS.levels[1] - CS.levels[0]
        cliplower = CS.zmin < vmin
        clipupper = CS.zmax > vmax
        noextend = kwargs.get("extend") == "neither"
    
        # set the colorbar boundaries
        boundaries = arange((floor(vmin/step)-1+1*(cliplower and noextend))
                            * step, (ceil(vmax/step)+1-1*(clipupper and noextend))*step, step)
        kwargs["boundaries"] = boundaries
    
        # if the z-values are outside the colorbar range, add extend marker(s)
        # This behavior can be disabled by providing extend="neither" to the function call
        if kwargs.get("extend") in [None, "min", "max"]:
            extend_min = cliplower or kwargs.get("extend") == "min"
            extend_max = clipupper or kwargs.get("extend") == "max"
            if extend_min and extend_max:
                kwargs["extend"] = "both"
            elif extend_min:
                kwargs["extend"] = "min"
            elif extend_max:
                kwargs["extend"] = "max"
        return fig.colorbar(m, ax=ax, **kwargs)
    
    
    fig, ax = plt.subplots()
    cb = ax.tricontourf(reflect, emiss, tempdiff, 200, 
                        vmin=-800, vmax=800,
                        cmap="seismic")
    cbar = clippedcolorbar(cb, fig, ax, extend="neither")
    cbar.set_label(r"z", rotation=270, labelpad=13)
    ax.set_xlabel("x")
    ax.set_ylabel("y")
    ax.set_xlim([0.6, 1])
    ax.set_ylim([0, 0.4])
    fig.show()
    

    Also, I'm not sure if you shared a different dataset than the one used to make your plot because the contour levels are slightly different, even when I run your exact code.