Skip to content

Commit 85acf5c

Browse files
committed
Permit loading arbitrary colormap files
1 parent c639612 commit 85acf5c

File tree

1 file changed

+61
-47
lines changed

1 file changed

+61
-47
lines changed

proplot/styletools.py

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,9 +1561,10 @@ def Colormap(*args, name=None, listmode='perceptual',
15611561
save=False, save_kw=None,
15621562
**kwargs):
15631563
"""
1564-
Function for generating and merging colormaps in a variety of ways;
1565-
used to interpret the `cmap` and `cmap_kw` arguments when passed to
1566-
any plotting method wrapped by `~proplot.wrappers.cmap_wrapper`.
1564+
Generates or retrieves colormaps and optionally merges and manipulates
1565+
them in a variety of ways; used to interpret the `cmap` and `cmap_kw`
1566+
arguments when passed to any plotting method wrapped by
1567+
`~proplot.wrappers.cmap_wrapper`.
15671568
15681569
Parameters
15691570
----------
@@ -1574,6 +1575,8 @@ def Colormap(*args, name=None, listmode='perceptual',
15741575
15751576
* If `~matplotlib.colors.Colormap` or a registered colormap name, the
15761577
colormap is simply returned.
1578+
* If a filename string with valid extension, the colormap data will
1579+
be loaded. See `register_cmaps` and `register_cycles`.
15771580
* If RGB tuple or color string, a `PerceptuallyUniformColormap` is
15781581
generated with `~PerceptuallyUniformColormap.from_color`. If the
15791582
string ends in ``'_r'``, the monochromatic map will be *reversed*,
@@ -1655,11 +1658,19 @@ def Colormap(*args, name=None, listmode='perceptual',
16551658
cmaps = []
16561659
tmp = '_no_name' # name required, but we only care about name of final merged map
16571660
for i,cmap in enumerate(args):
1658-
if isinstance(cmap,str):
1659-
try:
1660-
cmap = mcm.cmap_d[cmap]
1661-
except KeyError:
1662-
pass
1661+
# First load data
1662+
# TODO: Document how 'listmode' also affects loaded files
1663+
if isinstance(cmap, str):
1664+
if '.' in cmap:
1665+
if os.path.isfile(os.path.expanduser(cmap)):
1666+
tmp, cmap = _load_cmap_cycle(cmap, cmap=(listmode != 'listed'))
1667+
else:
1668+
raise FileNotFoundError(f'Colormap or cycle file {cmap!r} not found.')
1669+
else:
1670+
try:
1671+
cmap = mcm.cmap_d[cmap]
1672+
except KeyError:
1673+
pass
16631674
# Properties specific to each map
16641675
ireverse = False if not np.iterable(reverse) else reverse[i]
16651676
ileft = None if not np.iterable(left) else left[i]
@@ -1678,7 +1689,10 @@ def Colormap(*args, name=None, listmode='perceptual',
16781689
cmap = PerceptuallyUniformColormap.from_hsl(tmp, **cmap)
16791690
# List of color tuples or color strings, i.e. iterable of iterables
16801691
elif not isinstance(cmap, str) and np.iterable(cmap) and all(np.iterable(color) for color in cmap):
1681-
cmap = [to_rgb(color, cycle=cycle) for color in cmap] # transform C0, C1, etc. to actual names
1692+
try:
1693+
cmap = [to_rgb(color, cycle=cycle) for color in cmap] # transform C0, C1, etc. to actual names
1694+
except (ValueError, TypeError):
1695+
pass # raise error later on
16821696
if listmode == 'listed':
16831697
cmap = ListedColormap(cmap, tmp)
16841698
elif listmode == 'linear':
@@ -2245,13 +2259,14 @@ def _get_data_paths(dirname):
22452259
paths.insert(0, ipath)
22462260
return paths
22472261

2248-
def _read_cmap_cycle_data(filename):
2262+
def _load_cmap_cycle(filename, cmap=False):
22492263
"""
22502264
Helper function that reads generalized colormap and color cycle files.
22512265
"""
2252-
empty = (None, None, None)
2266+
N = rcParams['image.lut'] # query this when register function is called
2267+
filename = os.path.expanduser(filename)
22532268
if os.path.isdir(filename): # no warning
2254-
return empty
2269+
return None, None
22552270

22562271
# Directly read segmentdata json file
22572272
# NOTE: This is special case! Immediately return name and cmap
@@ -2260,17 +2275,15 @@ def _read_cmap_cycle_data(filename):
22602275
if ext == 'json':
22612276
with open(filename, 'r') as f:
22622277
data = json.load(f)
2263-
N = rcParams['image.lut']
22642278
if 'red' in data:
2265-
cmap = LinearSegmentedColormap(name, data, N=N)
2279+
data = LinearSegmentedColormap(name, data, N=N)
22662280
else:
22672281
kw = {}
22682282
for key in ('space', 'gamma1', 'gamma2'):
22692283
kw[key] = data.pop(key, None)
2270-
cmap = PerceptuallyUniformColormap(name, data, N=N, **kw)
2284+
data = PerceptuallyUniformColormap(name, data, N=N, **kw)
22712285
if name[-2:] == '_r':
2272-
cmap = cmap.reversed(name[:-2])
2273-
return name, None, cmap
2286+
data = data.reversed(name[:-2])
22742287

22752288
# Read .rgb, .rgba, .xrgb, and .xrgba files
22762289
elif ext in ('txt', 'rgb', 'xrgb', 'rgba', 'xrgba'):
@@ -2283,12 +2296,12 @@ def _read_cmap_cycle_data(filename):
22832296
data = [[float(num) for num in line] for line in data]
22842297
except ValueError:
22852298
warnings.warn(f'Failed to load "{filename}". Expected a table of comma or space-separated values.')
2286-
return empty
2299+
return None, None
22872300
# Build x-coordinates and standardize shape
22882301
data = np.array(data)
22892302
if data.shape[1] != len(ext):
22902303
warnings.warn(f'Failed to load "{filename}". Got {data.shape[1]} columns, but expected {len(ext)}.')
2291-
return empty
2304+
return None, None
22922305
if ext[0] != 'x': # i.e. no x-coordinates specified explicitly
22932306
x = np.linspace(0, 1, data.shape[0])
22942307
else:
@@ -2301,16 +2314,16 @@ def _read_cmap_cycle_data(filename):
23012314
xmldoc = etree.parse(filename)
23022315
except IOError:
23032316
warnings.warn(f'Failed to load "{filename}".')
2304-
return empty
2317+
return None, None
23052318
x, data = [], []
23062319
for s in xmldoc.getroot().findall('.//Point'):
23072320
# Verify keys
23082321
if any(key not in s.attrib for key in 'xrgb'):
23092322
warnings.warn(f'Failed to load "{filename}". Missing an x, r, g, or b specification inside one or more <Point> tags.')
2310-
return empty
2323+
return None, None
23112324
if 'o' in s.attrib and 'a' in s.attrib:
23122325
warnings.warn(f'Failed to load "{filename}". Contains ambiguous opacity key.')
2313-
return empty
2326+
return None, None
23142327
# Get data
23152328
color = []
23162329
for key in 'rgbao': # o for opacity
@@ -2322,7 +2335,7 @@ def _read_cmap_cycle_data(filename):
23222335
# Convert to array
23232336
if not all(len(data[0]) == len(color) for color in data):
23242337
warnings.warn(f'File {filename} has some points with alpha channel specified, some without.')
2325-
return empty
2338+
return None, None
23262339

23272340
# Read hex strings
23282341
elif ext == 'hex':
@@ -2331,26 +2344,36 @@ def _read_cmap_cycle_data(filename):
23312344
data = re.findall('#[0-9a-fA-F]{6}', string) # list of strings
23322345
if len(data) < 2:
23332346
warnings.warn(f'Failed to load "{filename}".')
2334-
return empty
2347+
return None, None
23352348
# Convert to array
23362349
x = np.linspace(0, 1, len(data))
23372350
data = [to_rgb(color) for color in data]
23382351
else:
23392352
warnings.warn(f'Colormap or cycle file {filename!r} has unknown extension.')
2340-
return empty
2353+
return None, None
23412354

23422355
# Standardize and reverse if necessary to cmap
2343-
x, data = np.array(x), np.array(data)
2344-
x = (x - x.min()) / (x.max() - x.min()) # for some reason, some aren't in 0-1 range
2345-
if (data > 2).any(): # from 0-255 to 0-1
2346-
data = data/255
2347-
if name[-2:] == '_r':
2348-
name = name[:-2]
2349-
data = data[::-1,:]
2350-
x = 1 - x[::-1]
2351-
2352-
# Return data
2353-
return name, x, data
2356+
# TODO: Document the fact that filenames ending in _r return a reversed
2357+
# version of the colormap stored in that file.
2358+
if isinstance(data, LinearSegmentedColormap):
2359+
if not cmap:
2360+
warnings.warn(f'Failed to load {filename!r} as color cycle.')
2361+
return None, None
2362+
else:
2363+
x, data = np.array(x), np.array(data)
2364+
x = (x - x.min()) / (x.max() - x.min()) # for some reason, some aren't in 0-1 range
2365+
if (data > 2).any(): # from 0-255 to 0-1
2366+
data = data/255
2367+
if name[-2:] == '_r':
2368+
name = name[:-2]
2369+
data = data[::-1,:]
2370+
x = 1 - x[::-1]
2371+
if cmap:
2372+
data = [(x,color) for x,color in zip(x,data)]
2373+
data = LinearSegmentedColormap.from_list(name, data, N=N)
2374+
2375+
# Return colormap or data
2376+
return name, data
23542377

23552378
@_timer
23562379
def register_cmaps():
@@ -2398,17 +2421,11 @@ def register_cmaps():
23982421
]
23992422

24002423
# Add colormaps from ProPlot and user directories
2401-
N = rcParams['image.lut'] # query this when register function is called
24022424
for path in _get_data_paths('cmaps'):
24032425
for filename in sorted(glob.glob(os.path.join(path, '*'))):
2404-
name, x, data = _read_cmap_cycle_data(filename)
2426+
name, cmap = _load_cmap_cycle(filename, cmap=True)
24052427
if name is None:
24062428
continue
2407-
if isinstance(data, LinearSegmentedColormap):
2408-
cmap = data
2409-
else:
2410-
data = [(x,color) for x,color in zip(x,data)]
2411-
cmap = LinearSegmentedColormap.from_list(name, data, N=N)
24122429
mcm.cmap_d[name] = cmap
24132430
cmaps.append(name)
24142431
# Add cyclic attribute
@@ -2445,12 +2462,9 @@ def register_cycles():
24452462
icycles = {}
24462463
for path in _get_data_paths('cycles'):
24472464
for filename in sorted(glob.glob(os.path.join(path, '*'))):
2448-
name, _, data = _read_cmap_cycle_data(filename)
2465+
name, data = _load_cmap_cycle(filename, cmap=False)
24492466
if name is None:
24502467
continue
2451-
if isinstance(data, LinearSegmentedColormap):
2452-
warnings.warn(f'Failed to load {filename!r} as color cycle.')
2453-
continue
24542468
icycles[name] = data
24552469

24562470
# Register cycles as ListedColormaps

0 commit comments

Comments
 (0)