@@ -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
23562379def 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