Cmaptools
A convenient package to read GMT style cpt-files to matplotlib cmaps and mimic the dynamic scaling around a hinge point
Install / Use
/learn @shaharkadmiel/CmaptoolsREADME
cmaptools
A convenient package to read GMT style cpt-files to matplotlib cmaps and mimic the dynamic scaling around a hinge point. See GMT: 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:
Install
Clone the repo by typing:
$ git clone https://github.com/shaharkadmiel/cmaptools.git
and install with:
$ cd cmaptools
$ pip install -e .
Demo
import matplotlib.pyplot as plt
from cmaptools import readcpt, joincmap, DynamicColormap
Generic Mapping Tools style cpt files
A GMT color palette table (cpt) file is read and converted to a matplotlib colormap instance with readcpt. A DynamicColormap instance is returned if the colormap is diverging (vmin < hinge < vmax) or a LinearSegmentedColormap 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, the globe.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/255
0 51/102/0 0.01 51/204/102
...
0.95 white 1 white
The preview method of the DynamicColormap 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 between vmin=-10 and vmax=8. Each of the domains is scaled separately, keeping the hinge at 0 (the original hinge value).
Non GMT style cpt files
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, like mby.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 71
0 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, setting hinge=None results in a LinearSegmentedColormap with the color discontinuity somewhere around 0.61:
from matplotlib.colorbar import ColorbarBase
cptfile = '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 assumes hinge=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
Joining two colormaps
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 that cmap1 and cmap2 are normal colormap instances of LinearSegmentedColormap while the joined colormap is DynamicColormap.
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'>

Examples with topography and bathymetry
Here I use a subsampled grid of the GEBCO2019 15 arc-second grid.
from xarray import open_dataset
from matplotlib.colors import LightSource
topo = 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.0
Data 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(
Related Skills
node-connect
341.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
84.4kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
84.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
341.0kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
