- Notifications
You must be signed in to change notification settings - Fork1
A convenient package to read GMT style cpt-files to matplotlib cmaps and mimic the dynamic scaling around a hinge point
License
shaharkadmiel/cmaptools
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A convenient package to read GMT style cpt-files to matplotlib cmaps andmimic the dynamic scaling around a hinge point. SeeGMT: Master (dynamic) CPTs for more details...
Some colormaps are designed with a color discontinuity to emphasize the boundary between two different domains, for instance between bathymetry and topography. When scaling a dynamic colormap, the lower (v < hinge
) and upper (v > hinge
) parts are scaled separately. Here is an example:
Clone the repo by typing:
$ git clone https://github.com/shaharkadmiel/cmaptools.git
and install with:
$cd cmaptools$ pip install -e.
importmatplotlib.pyplotaspltfromcmaptoolsimportreadcpt,joincmap,DynamicColormap
A GMT color palette table (cpt) file is read and converted to a matplotlib colormap instance withreadcpt
. ADynamicColormap
instance is returned if the colormap is diverging (vmin < hinge < vmax
) or aLinearSegmentedColormap
instance if the colormap is sequential.
cptfile='globe.cpt'cmap=readcpt(cptfile)print(cmap.name,cmap.__class__)cmap.preview(-10,8)
globe <class 'cmaptools.DynamicColormap'>
In the above example, theglobe.cpt
cpt file bundled with GMT is normalized between -1 and 1 with a hinge at 0:
# header info......-1 153/0/255 -0.95 153/0/255...-0.02 241/252/255 0 241/252/2550 51/102/0 0.01 51/204/102...0.95 white 1 white
Thepreview
method of theDynamicColormap
class shows three representations of the colormap. The top colorbar shows how matplotlib would treat this colormap with out any knowledge of its dynamic properties. Matplotlib colormaps are normalized between 0 and 1 so the hinge value, which is in the middle of the original range, is naturally mapped to 0.5. The colorbar in the middle, shows the original normalization of the colormap and the bottom colorbar shows how this colormap would look like if scaled betweenvmin=-10
andvmax=8
. Each of the domains is scaled separately, keeping the hinge at 0 (the original hinge value).
There are many more sources for cpt files.cpt-city provides a collection of cpt files for various different applications including ones that are designed for bathymetry and topography, likemby.cpt
. This cpt file is not normalized, not symmetric around the color discontinuity and does not contain information about the hinge value in the header:
# header info......-8000 0 0 80 -6000 0 30 100...-10 176 226 255 0 0 97 710 0 97 71 50 16 123 48...4000 206 206 206 5000 255 255 255
Without proper normalization and scaling, this colormap is difficult to manipulate with regard to the color discontinuity. As there is no hinge information in the header, settinghinge=None
results in aLinearSegmentedColormap
with the color discontinuity somewhere around 0.61:
frommatplotlib.colorbarimportColorbarBasecptfile='mby.cpt'cmap=readcpt(cptfile,hinge=None)print(cmap.name,cmap.__class__)fig,ax=plt.subplots(1,figsize=(4,0.2))fig.subplots_adjust(0,0,1,1,hspace=3)fig.suptitle(cmap.name,y=2)ColorbarBase(ax,cmap,orientation='horizontal')ax.set_xlabel('no norm information')
mby <class 'matplotlib.colors.LinearSegmentedColormap'>
readcpt
by default assumeshinge=0
so if the values in a cpt file range from negative to positive values, color segments are parsed and scaled to account for the hinge value even if the range is not symmetric around it:
cptfile='mby.cpt'cmap=readcpt(cptfile)print(cmap.name,cmap.__class__)cmap.preview(-10,8)
mby <class 'cmaptools.DynamicColormap'>
As before, the top colorbar shows the matplotlib version of the scaled colormap so that the hinge, eventhough not in the middle of the range in the original cpt file, is mapped to 0.5. Middle colorbar shows the colormap with its original norm and the bottom colorbar is shows how this colormap would look like if scaled between vmin=-10 and vmax=8
It is also possible to join two colormaps together:
cptfile1='seafloor.cpt'cmap1=readcpt(cptfile1)print(cmap1.name,cmap1.__class__)cptfile2='dem2.cpt'cmap2=readcpt(cptfile2)print(cmap2.name,cmap2.__class__)cmap=joincmap(cmap1,cmap2)print(cmap.name,cmap.__class__)cmap.preview(-10,8)
seafloor <class 'matplotlib.colors.LinearSegmentedColormap'>dem2 <class 'matplotlib.colors.LinearSegmentedColormap'>seafloor->dem2 <class 'cmaptools.DynamicColormap'>
Note thatcmap1
andcmap2
are normal colormap instances ofLinearSegmentedColormap
while the joined colormap isDynamicColormap
.
This can be done with matplotlib colormaps as well, here scaling between -8 and 5:
cmap=joincmap('Blues_r','pink_r')print(cmap.name,cmap.__class__)cmap.preview(-8,5,0)
Blues_r->pink_r <class 'cmaptools.DynamicColormap'>
or a matplotlib colormap and a GMT cpt file:
cmap1=plt.get_cmap('cool_r')print(cmap1.name,cmap1.__class__)cptfile2='dem2.cpt'cmap2=readcpt(cptfile2)print(cmap2.name,cmap2.__class__)cmap=joincmap(cmap1,cmap2)print(cmap.name,cmap.__class__)cmap.preview(-10,8)
cool_r <class 'matplotlib.colors.LinearSegmentedColormap'>dem2 <class 'matplotlib.colors.LinearSegmentedColormap'>cool_r->dem2 <class 'cmaptools.DynamicColormap'>
Any colormap can be made dynamic:
cmap=plt.get_cmap('seismic')print(cmap.name,cmap.__class__)cmap=DynamicColormap(cmap)print(cmap.name,cmap.__class__)cmap.preview(-8,2,0)
seismic <class 'matplotlib.colors.LinearSegmentedColormap'>seismic <class 'cmaptools.DynamicColormap'>
this is handy when the data being plotted is not symmetric around the hinge value.
The hinge value can set to other than 0
cmap=plt.get_cmap('jet')print(cmap.name,cmap.__class__)cmap=DynamicColormap(cmap)print(cmap.name,cmap.__class__)cmap.preview(-8,5,2)
jet <class 'matplotlib.colors.LinearSegmentedColormap'>jet <class 'cmaptools.DynamicColormap'>
Here I use a subsampled grid of theGEBCO2019 15 arc-second grid.
fromxarrayimportopen_datasetfrommatplotlib.colorsimportLightSourcetopo=open_dataset('GEBCO_2019_960AS.nc')print(topo)ls=LightSource()
<xarray.Dataset>Dimensions: (extent: 4, lat: 675, lon: 1350)Coordinates: * lat (lat) float64 -89.87 -89.6 -89.33 -89.07 ... 89.33 89.6 89.87 * lon (lon) float64 -179.9 -179.6 -179.3 -179.1 ... 179.3 179.6 179.9 * extent (extent) float64 -180.0 180.0 -90.0 90.0Data variables: elevation (lat, lon) int16 ... crs |S1 ... tlx float64 ... tly float64 ... dx float64 ... dy float64 ...Attributes: Conventions: CF-1.5 GDAL: GDAL 2.4.1, released 2019/03/15 history: Subsampled from 15 to 960 arc-second and forced to INT16 wi... title: The GEBCO_2019 Grid - a continuous terrain model for oceans... source: A subsampled version of the 15 arc-second GEBCO_2019 grid d...
Using the same dynamic colormaps generated above:
cptfile='globe.cpt'cmap=readcpt(cptfile)cmap.set_range(-9e3,6e3)plt.figure(figsize=(6,3))rgb=ls.shade(topo.elevation[::-1].data,cmap,cmap.norm,'overlay',vert_exag=0.01)plt.imshow(rgb,cmap,cmap.norm,extent=topo.extent,aspect='auto',interpolation='bilinear')plt.colorbar(extend='both')plt.contour(topo.elevation,levels=[0],colors='k',linewidths=0.25,extent=topo.extent)
cptfile='mby.cpt'cmap=readcpt(cptfile)cmap.set_range(-11e3,6e3)plt.figure(figsize=(6,3))rgb=ls.shade(topo.elevation[::-1].data,cmap,cmap.norm,'overlay',vert_exag=0.01)plt.imshow(rgb,cmap,cmap.norm,extent=topo.extent,aspect='auto',interpolation='bilinear')plt.colorbar(extend='both')plt.contour(topo.elevation,levels=[0],colors='k',linewidths=0.25,extent=topo.extent)
cptfile1='seafloor.cpt'cmap1=readcpt(cptfile1)cptfile2='dem2.cpt'cmap2=readcpt(cptfile2)cmap=joincmap(cmap1,cmap2)cmap.set_range(-9e3,6e3)plt.figure(figsize=(6,3))rgb=ls.shade(topo.elevation[::-1].data,cmap,cmap.norm,'overlay',vert_exag=0.01)plt.imshow(rgb,cmap,cmap.norm,extent=topo.extent,aspect='auto',interpolation='bilinear')plt.colorbar(extend='both')plt.contour(topo.elevation,levels=[0],colors='k',linewidths=0.25,extent=topo.extent)
cmap=joincmap('Blues_r','pink_r')cmap.set_range(-9e3,5e3)plt.figure(figsize=(6,3))rgb=ls.shade(topo.elevation[::-1].data,cmap,cmap.norm,'overlay',vert_exag=0.01)plt.imshow(rgb,cmap,cmap.norm,extent=topo.extent,aspect='auto',interpolation='bilinear')plt.colorbar(extend='both')plt.contour(topo.elevation,levels=[0],colors='k',linewidths=0.25,extent=topo.extent)
make the data:
importnumpyasnpfromscipy.statsimportmultivariate_normaldelta=0.025x=y=np.arange(-3.0,3.0,delta)extent= [-3,3]*2xx,yy=np.meshgrid(x,y)z1=multivariate_normal([0,0], [1,1]).pdf(np.dstack((xx,yy)))z2=multivariate_normal([1,1], [2,0.25]).pdf(np.dstack((xx,yy)))z=z2-2*z1z/=z.max()
plot without normalization:
plt.imshow(z[::-1],'seismic',aspect=1,interpolation='bilinear',extent=extent)plt.colorbar()plt.contour(z,levels=[0],colors='k',linewidths=0.25,extent=extent)
Note how the colormap diverges from white but because the min/max of the data is not symmetric around 0, white is not at the center.
One way of handling this is to setvmin
andvmax
to the-
and+
of theabs(z).max()
:
plt.imshow(z[::-1],'seismic',aspect=1,interpolation='bilinear',extent=extent,vmin=-np.abs(z).max(),vmax=np.abs(z).max())plt.colorbar()plt.contour(z,levels=[0],colors='k',linewidths=0.25,extent=extent)
However, the positive part of the data is now pale as some dynamic range is lost.
Making theseismic
colormap dynamic, it is now possible to set the range to themin
andmax
of the data:
cmap=plt.get_cmap('seismic')cmap=DynamicColormap(cmap)cmap.set_range(z.min(),z.max())plt.imshow(z[::-1],cmap,cmap.norm,aspect=1,interpolation='bilinear',extent=extent)plt.colorbar()plt.contour(z,levels=[0],colors='k',linewidths=0.25,extent=extent)