"""
--------------------------------------------------------------------------------
Name: get_cpt matplotlib colormap utility
Purpose: an easy way to fetch .cpt colormap files, based on pycpt
Created: 2020.03
Copyright: (c) Dimitrios Bouziotas (bouziot)
Licence: GGNU General Public License v3 (GPL-3)
-You may freely copy, distribute and modify the software, in accordance with the provided license.
-You may not sublicense or hold the original author liable. This software comes with no warranty at all.
-Active contributions, forks and redevelopments are welcome.
-If you would like to include this software in your work, please reference it using the zenodo or github link. Please
also reference the original pycpt package (https://github.com/j08lue/pycpt)
--------------------------------------------------------------------------------
"""
__version__ = '0.1.0'
__copyright__ = """(c) 2020 Dimitrios Bouziotas"""
__license__ = "GGNU General Public License v3 (GPL-3)"
import os
import numpy as np
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
from urllib.request import urlopen
basedir = os.path.join(os.getcwd(),'cpt') # base path from where cpt files are fetched
[docs]
def get_cmap(cpt_path, name=None, method='cdict', ret_cmap_type='LinearSegmented', N=None):
"""Get the cpt colormap as a LinearSegmented colormap. Utilizes the gmtColormap_openfile parser.
Parameters
----------
cpt_path : str, with one of the following options:
- the full dir path to a .cpt file
- the filename of a .cpt file that is in the local repo (check get_cpt.basedir)
- a url.
name : str, optional
colormap name
if not provided, the name will be derived automatically using _getname()
method: str, optional
Choose between 'cdict' and 'list'. The first one fetches all info from the .cpt file. The latter
allows you to control the number of colors to sample from. Check gmtColormap_openfile for more info.
N: int, optional
the number of colors in the colormap to be returned. Can define the granularity of the returned colormap.
Only useful when method='list'
"""
# first get name
if name is None:
name = _getname(cpt_path)
# case URL
if 'http://' in cpt_path or 'https://' in cpt_path:
with urlopen(cpt_path) as f:
return gmtColormap_openfile(f, name=name, method=method, N=N, ret_cmap_type=ret_cmap_type)
# case FULLPATH OR NAME
else:
with open(cpt_path) as f:
return gmtColormap_openfile(f, name=name, method=method, N=N, ret_cmap_type=ret_cmap_type)
[docs]
def get_listed_cmap(cpt_path, name=None, N=None):
"""Get the cpt colormap as a ListedColormap. Utilizes the gmtColormap_openfile parser.
Parameters
----------
cpt_path : str, with one of the following options:
- the full dir path to a .cpt file
- the filename of a .cpt file that is in the local repo (check get_cpt.basedir)
- a url
name : str, optional
colormap name
if not provided, the name will be derived automatically using _getname()
N: int, optional
the number of colors in the colormap to be returned. Leave None to derive the colors from the .cpt file.
If you use a number less than the colors included in that file, a subset of colors will be returned.
"""
# first get name
if name is None:
name = _getname(cpt_path)
# case URL
if 'http://' in cpt_path or 'https://' in cpt_path:
with urlopen(cpt_path) as f:
return gmtColormap_openfile(f, name=name, method='list', N=N, ret_cmap_type='Listed')
# case FULLPATH OR NAME
else:
with open(cpt_path) as f:
return gmtColormap_openfile(f, name=name, method='list', N=N, ret_cmap_type='Listed')
def _getname(cpt_path):
"""Internal function, fetches the name from a cpt filepath or url.
Templates:
'my.mby.cpt' -> 'my_mby' # NAME
r'D:\matplotlib colormaps - cpt-city\cpt\mby.cpt' -> 'mby' # FULLPATH
'http://soliton.vm.bytemark.co.uk/pub/cpt-city/cmocean/haline.cpt' -> 'haline' # URL
"""
if 'http://' in cpt_path or 'https://' in cpt_path: # CASE URL
return '_'.join(cpt_path.split(r'/')[-1].split('.')[:-1])
else:
if os.path.exists(cpt_path):
return cpt_path
else:
raise FileNotFoundError("CPT file {} not found".format(cpt_path))
[docs]
def gmtColormap_openfile(cptf, name=None, method='cdict', N=None, ret_cmap_type='LinearSegmented'):
"""Read a GMT color map from an OPEN cpt file
Edited by: bouziot, 2020.03
Parameters
----------
cptf : str, open file or url handle
path to .cpt file
name : str, optional
name for color map
if not provided, the file name will be used
method : str, suggests the method to use.
If method = 'cdict', generates the LinearSegmentedColormap using a color dictionary (cdict), disregarding any value in N.
If method = 'list', generates the LinearSegmentedColor using the .from_list() method, passing a list of (value, (r,g,b)) tuples obtained from the cpt file. This allows the selection of colormap resolution by the user, using the N parameter
N : int, the number of colors in the colormap. Only useful when method='list'.
ret_cmap_type: str, the type of matplotlib cmap object to be returned. Accepts either 'LinearSegmented', which returns a matplotlib.colors.LinearSegmentedColormap, or 'Listed', which returns a ListedColormap
In case 'Listed' is selected, the method argument from the user is ignored and method is set to 'list' ('Linear' doesn't work with 'cdict').
N is then passed to matplotlib.colors.ListedColormap().
- If N is set to None: all colors of the cpt file will be returned as a list.
- In case of a user-defined N, colors will be truncated or extended by repetition (see matplotlib.colors.ListedColormap).
Returns
-------
a matplotlib colormap object (matplotlib.colors.LinearSegmentedColormap or matplotlib.colors.ListedColormap)
Credits
-------
This function originally appears in pycpt, extensive edits from bouziot, 2020.03
Original work in: https://github.com/j08lue/pycpt
LOG OF EDITS (2020.03):
- Fixed bug when parsing non-split '#' lines in .cpt files
- Fixed bug - not identifying the colorModel '#' line correctly
- Fixed binary comparison performance (introduced in python 3)
- Added functionality to return ListedColormaps and cmaps with custom colors (method, ret_cmap_type args)
- Added global name handling externally (_getname() func)
"""
methodnames = ['cdict', 'list'] # accepted method arguments
ret_cmap_types = ['LinearSegmented', 'Listed', 'raw'] # accepted return matplotlib colormap types
# generate cmap name
if name is None:
name = _getname(cptf.name)
# name = '_'.join(os.path.basename(cptf.name).split('.')[:-1])
# process file
x = []
r = []
g = []
b = []
lastls = None
for l in cptf.readlines():
ls = l.split()
# skip empty lines
if not ls:
continue
# parse header info
# this leads to mistakes in some files...
# if ls[0] in ["#", b"#"]: # '#' is not always separated from other letters in some cases...
# if ls[-1] in ["HSV", b"HSV"]:
# colorModel = "HSV"
# else:
# colorModel = "RGB"
# continue
# byte comparison is not feasible in python 3
if (isinstance(l, bytes) and l.decode('utf-8')[0] in ["#", b"#"]) or (isinstance(l, str) and l[0] in ["#", b"#"]):
if ls[-1] in ["HSV", b"HSV"]:
colorModel = "HSV"
continue
elif ls[-1] in ["RGB", b"RGB"]:
colorModel = "RGB"
continue
else: # case rogue comment, ignore
continue
# skip BFN info
if ls[0] in ["B", b"B", "F", b"F", "N", b"N"]:
continue
# parse color vectors
x.append(float(ls[0]))
r.append(float(ls[1]))
g.append(float(ls[2]))
b.append(float(ls[3]))
# save last row
lastls = ls
# check if last endrow has the same color, if not, append
if not ((float(lastls[5]) == r[-1]) and (float(lastls[6]) == g[-1]) and (float(lastls[7]) == b[-1])):
x.append(float(lastls[4]))
r.append(float(lastls[5]))
g.append(float(lastls[6]))
b.append(float(lastls[7]))
x = np.array(x)
r = np.array(r)
g = np.array(g)
b = np.array(b)
if colorModel == "HSV":
for i in range(r.shape[0]):
# convert HSV to RGB
rr,gg,bb = colorsys.hsv_to_rgb(r[i]/360., g[i], b[i])
r[i] = rr ; g[i] = gg ; b[i] = bb
elif colorModel == "RGB":
r /= 255.
g /= 255.
b /= 255.
red = []
blue = []
green = []
xNorm = (x - x[0])/(x[-1] - x[0])
# return colormap
if method == 'cdict' and ret_cmap_type == 'LinearSegmented':
# generate cdict
for i in range(len(x)):
red.append([xNorm[i],r[i],r[i]])
green.append([xNorm[i],g[i],g[i]])
blue.append([xNorm[i],b[i],b[i]])
cdict = dict(red=red,green=green,blue=blue)
#return cdict
return mcolors.LinearSegmentedColormap(name=name,segmentdata=cdict)
elif method == 'list' and ret_cmap_type == 'LinearSegmented':
# generate list of values in the form of (value, c)
outlist = []
for i in range(len(x)):
tup = (xNorm[i], (r[i],g[i],b[i]))
outlist.append(tup)
if N and type(N) == int:
#return outlist
return mcolors.LinearSegmentedColormap.from_list(name, outlist, N=N)
else:
raise TypeError("Using the method 'list' requires you to set a number of colors N.")
elif ret_cmap_type == 'Listed':
# generate list of values and return it as ListedColormap
# returns both colors and the normalized positions (pos) where colors change, in the form of two outputs pos, colors
pos_out = []
colors_out = []
for i in range(len(x)):
pos_out.append(xNorm[i]) # list of positions
colors_out.append(mcolors.to_hex( (r[i],g[i],b[i]))) # list of colors
# return pos, color pairs
print(colors_out)
if N and type(N) == int and N<=len(colors_out):
pos_out = pos_out[:N] #truncate positions to N
return pos_out, mcolors.ListedColormap(colors_out, name=name, N=N)
elif N is None:
return pos_out, mcolors.ListedColormap(colors_out, name=name)
else:
raise TypeError("N has to be a number of colors that is less than the actual colors found in the .cpt file (" + str(len(colors_out)) + " colors found).")
elif ret_cmap_type == 'raw':
pos_out = []
colors_out = []
for i in range(len(x)):
pos_out.append(xNorm[i]) # list of positions
colors_out.append((r[i]*255, g[i]*255, b[i]*255))
return pos_out, colors_out
else:
raise TypeError("method has to be one of the arguments: " + str(methodnames) + " and ret_cmap_type has to be one of the arguments: " + str(ret_cmap_types))
[docs]
def plot_cmaps(cmap_list, width=6, cmap_height=0.5, axes_off=False):
"""Plot a colormap or list of colormaps with their names.
Parameters
-------
cmap_list (str, cmap object or list of cmap objects anr strings): a list of colormaps to plot, either as cmap objects OR as preinstalled matplotlib colormap strings
width (float): width of plot
cmap_height (float): height of each colormap in plot
axes_off (bool): boolean to erase axes
Returns
-------
a matplotlib figure object (matplotlib.figure.Figure)
Credits
-------
This function originally appears in pycpt, slight edits from bouziot, 2020.03
https://github.com/j08lue/pycpt
http://matplotlib.org/examples/color/colormaps_reference.html
"""
if not isinstance(cmap_list, list):
cmap_list = [cmap_list] # make subscriptable
gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))
fig, axes = plt.subplots(nrows=len(cmap_list), figsize=(width,cmap_height*len(cmap_list)))
fig.subplots_adjust(top=1, bottom=0, left=0, right=0.9)
if len(cmap_list) == 1:
cmap = cmap_list[0]
# CASE ONE CMAP
if isinstance(cmap, str):
cmap = plt.get_cmap(cmap)
axes.imshow(gradient, aspect='auto', cmap=cmap)
pos = list(axes.get_position().bounds)
x_text = pos[0] + pos[2] + 0.02
y_text = pos[1] + pos[3]/2.
fig.text(x_text, y_text, cmap.name, va='center', ha='left', fontsize=12)
if axes_off:
axes.set_axis_off()
return fig
else:
# CASE MULTIPLE CMAPS
for i, cmap in enumerate(cmap_list):
if isinstance(cmap, str):
cmap = plt.get_cmap(cmap)
axes[i].imshow(gradient, aspect='auto', cmap=cmap)
pos = list(axes[i].get_position().bounds)
x_text = pos[0] + pos[2] + 0.02
y_text = pos[1] + pos[3]/2.
fig.text(x_text, y_text, cmap.name, va='center', ha='left', fontsize=12)
# Turn off *all* ticks & spines, not just the ones with colormaps.
if axes_off:
for ax in axes:
ax.set_axis_off()
return fig
if __name__ == '__main__':
#tests
# test 1: FULL PATH, LinearSegmented, method cdict
cpt_path = r'D:\Users\bouzidi\Desktop\matplotlib colormaps - cpt-city\cpt'
cpt_fullpath = os.path.join(cpt_path, 'mby.cpt')
a = get_cmap(cpt_fullpath)
print(a)
# test 2: LOCAL FILE, CHANGE BASEDIR
basedir = r'D:\Users\bouzidi\Desktop\matplotlib colormaps - cpt-city\test\new_ctp'
print(basedir)
myctp2 = 'purple-orange-d15.cpt'
pos, b = get_listed_cmap(myctp2)
# test 3: url
myurl = 'http://soliton.vm.bytemark.co.uk/pub/cpt-city/km/purple-orange-d15.cpt'
c = get_cmap(myurl)
print(c)
print(c.name)
# test 4: plots
fig = plot_cmaps([a,b,c])
plt.show()