Skip to content

Commit 7deb1b7

Browse files
committed
Add from_file static methods, file loading cleanup
1 parent 8d9a171 commit 7deb1b7

File tree

1 file changed

+137
-88
lines changed

1 file changed

+137
-88
lines changed

proplot/styletools.py

Lines changed: 137 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -894,54 +894,6 @@ def xyy(ix, funcs=funcs):
894894
# Return copy
895895
return self.updated(name=name, segmentdata=segmentdata, **kwargs)
896896

897-
@staticmethod
898-
def from_list(name, colors, ratios=None, **kwargs):
899-
"""
900-
Make a `LinearSegmentedColormap` from a list of colors.
901-
902-
Parameters
903-
----------
904-
name : str
905-
The colormap name.
906-
colors : list of color-spec or (float, color-spec) tuples, optional
907-
If list of RGB[A] tuples or color strings, the colormap transitions
908-
evenly from ``colors[0]`` at the left-hand side to
909-
``colors[-1]`` at the right-hand side.
910-
911-
If list of (float, color-spec) tuples, the float values are the
912-
coordinate of each transition and must range from 0 to 1. This
913-
can be used to divide the colormap range unevenly.
914-
ratios : list of float, optional
915-
Relative extents of each color transition. Must have length
916-
``len(colors) - 1``. Larger numbers indicate a slower
917-
transition, smaller numbers indicate a faster transition.
918-
919-
Other parameters
920-
----------------
921-
**kwargs
922-
Passed to `LinearSegmentedColormap`.
923-
924-
Returns
925-
-------
926-
`LinearSegmentedColormap`
927-
The colormap.
928-
"""
929-
# Get coordinates
930-
coords = None
931-
if not np.iterable(colors):
932-
raise ValueError(f'Colors must be iterable, got colors={colors!r}')
933-
if (np.iterable(colors[0]) and len(colors[0]) == 2
934-
and not isinstance(colors[0], str)):
935-
coords, colors = zip(*colors)
936-
colors = [to_rgb(color, alpha=True) for color in colors]
937-
938-
# Build segmentdata
939-
keys = ('red', 'green', 'blue', 'alpha')
940-
cdict = {}
941-
for key, values in zip(keys, zip(*colors)):
942-
cdict[key] = _make_segmentdata_array(values, coords, ratios)
943-
return LinearSegmentedColormap(name, cdict, **kwargs)
944-
945897
def punched(self, cut=None, name=None, **kwargs):
946898
"""
947899
Return a version of the colormap with the center "punched out".
@@ -1238,6 +1190,77 @@ def updated(
12381190
cmap._rgba_over = self._rgba_over
12391191
return cmap
12401192

1193+
@staticmethod
1194+
def from_file(path):
1195+
"""
1196+
Load colormap from a file.
1197+
Valid file extensions are described in the below table.
1198+
1199+
===================== =============================================================================================================================================================================================================
1200+
Extension Description
1201+
===================== =============================================================================================================================================================================================================
1202+
``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes).'ColorBlind10':
1203+
``.xml`` XML files with ``<Point .../>`` entries specifying ``x``, ``r``, ``g``, ``b``, and optionally, ``a`` values, where ``x`` is the colormap coordinate and the rest are the RGB and opacity (or "alpha") values.
1204+
``.rgb`` 3-column table delimited by commas or consecutive spaces, each column indicating red, blue and green color values.
1205+
``.xrgb`` As with ``.rgb``, but with 4 columns. The first column indicates the colormap coordinate.
1206+
``.rgba``, ``.xrgba`` As with ``.rgb``, ``.xrgb``, but with a trailing opacity (or "alpha") column.
1207+
===================== =============================================================================================================================================================================================================
1208+
1209+
Parameters
1210+
----------
1211+
path : str
1212+
The file path.
1213+
""" # noqa
1214+
return _from_file(path, listed=False)
1215+
1216+
@staticmethod
1217+
def from_list(name, colors, ratios=None, **kwargs):
1218+
"""
1219+
Make a `LinearSegmentedColormap` from a list of colors.
1220+
1221+
Parameters
1222+
----------
1223+
name : str
1224+
The colormap name.
1225+
colors : list of color-spec or (float, color-spec) tuples, optional
1226+
If list of RGB[A] tuples or color strings, the colormap transitions
1227+
evenly from ``colors[0]`` at the left-hand side to
1228+
``colors[-1]`` at the right-hand side.
1229+
1230+
If list of (float, color-spec) tuples, the float values are the
1231+
coordinate of each transition and must range from 0 to 1. This
1232+
can be used to divide the colormap range unevenly.
1233+
ratios : list of float, optional
1234+
Relative extents of each color transition. Must have length
1235+
``len(colors) - 1``. Larger numbers indicate a slower
1236+
transition, smaller numbers indicate a faster transition.
1237+
1238+
Other parameters
1239+
----------------
1240+
**kwargs
1241+
Passed to `LinearSegmentedColormap`.
1242+
1243+
Returns
1244+
-------
1245+
`LinearSegmentedColormap`
1246+
The colormap.
1247+
"""
1248+
# Get coordinates
1249+
coords = None
1250+
if not np.iterable(colors):
1251+
raise ValueError(f'Colors must be iterable, got colors={colors!r}')
1252+
if (np.iterable(colors[0]) and len(colors[0]) == 2
1253+
and not isinstance(colors[0], str)):
1254+
coords, colors = zip(*colors)
1255+
colors = [to_rgb(color, alpha=True) for color in colors]
1256+
1257+
# Build segmentdata
1258+
keys = ('red', 'green', 'blue', 'alpha')
1259+
cdict = {}
1260+
for key, values in zip(keys, zip(*colors)):
1261+
cdict[key] = _make_segmentdata_array(values, coords, ratios)
1262+
return LinearSegmentedColormap(name, cdict, **kwargs)
1263+
12411264

12421265
class ListedColormap(mcolors.ListedColormap, _Colormap):
12431266
r"""
@@ -1410,6 +1433,29 @@ def updated(self, colors=None, name=None, N=None, *, alpha=None):
14101433
cmap._rgba_over = self._rgba_over
14111434
return cmap
14121435

1436+
@staticmethod
1437+
def from_file(path):
1438+
"""
1439+
Load color cycle from a file.
1440+
Valid file extensions are described in the below table.
1441+
1442+
===================== =============================================================================================================================================================================================================
1443+
Extension Description
1444+
===================== =============================================================================================================================================================================================================
1445+
``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes).'ColorBlind10':
1446+
``.xml`` XML files with ``<Point .../>`` entries specifying ``x``, ``r``, ``g``, ``b``, and optionally, ``a`` values, where ``x`` is the colormap coordinate and the rest are the RGB and opacity (or "alpha") values.
1447+
``.rgb`` 3-column table delimited by commas or consecutive spaces, each column indicating red, blue and green color values.
1448+
``.xrgb`` As with ``.rgb``, but with 4 columns. The first column indicates the colormap coordinate.
1449+
``.rgba``, ``.xrgba`` As with ``.rgb``, ``.xrgb``, but with a trailing opacity (or "alpha") column.
1450+
===================== =============================================================================================================================================================================================================
1451+
1452+
Parameters
1453+
----------
1454+
path : str
1455+
The file path.
1456+
""" # noqa
1457+
return _from_file(path, listed=True)
1458+
14131459

14141460
class PerceptuallyUniformColormap(LinearSegmentedColormap, _Colormap):
14151461
"""Similar to `~matplotlib.colors.LinearSegmentedColormap`, but instead
@@ -2098,12 +2144,16 @@ def Colormap(
20982144
# TODO: Document how 'listmode' also affects loaded files
20992145
if isinstance(cmap, str):
21002146
if '.' in cmap:
2101-
if os.path.isfile(os.path.expanduser(cmap)):
2102-
tmp, cmap = _load_cmap_cycle(
2103-
cmap, cmap=(listmode != 'listed'))
2104-
else:
2147+
isfile = os.path.isfile(os.path.expanduser(cmap))
2148+
if isfile:
2149+
if listmode == 'listed':
2150+
cmap = ListedColormap.from_file(cmap)
2151+
else:
2152+
cmap = LinearSegmentedColormap.from_file(cmap)
2153+
if not isfile or not cmap:
21052154
raise FileNotFoundError(
2106-
f'Colormap or cycle file {cmap!r} not found.'
2155+
f'Colormap or cycle file {cmap!r} not found '
2156+
'or failed to load.'
21072157
)
21082158
else:
21092159
try:
@@ -2742,31 +2792,30 @@ def _get_data_paths(dirname):
27422792
]
27432793

27442794

2745-
def _load_cmap_cycle(filename, cmap=False):
2746-
"""
2747-
Helper function that reads generalized colormap and color cycle files.
2748-
"""
2749-
N = rcParams['image.lut'] # query this when register function is called
2795+
def _from_file(filename, listed=False):
2796+
"""Read generalized colormap and color cycle files."""
27502797
filename = os.path.expanduser(filename)
27512798
if os.path.isdir(filename): # no warning
2752-
return None, None
2799+
return
27532800

27542801
# Directly read segmentdata json file
27552802
# NOTE: This is special case! Immediately return name and cmap
2803+
N = rcParams['image.lut']
27562804
name, ext = os.path.splitext(os.path.basename(filename))
27572805
ext = ext[1:]
2806+
cmap = None
27582807
if ext == 'json':
27592808
with open(filename, 'r') as f:
27602809
data = json.load(f)
27612810
kw = {}
27622811
for key in ('cyclic', 'gamma', 'gamma1', 'gamma2', 'space'):
27632812
kw[key] = data.pop(key, None)
27642813
if 'red' in data:
2765-
data = LinearSegmentedColormap(name, data, N=N)
2814+
cmap = LinearSegmentedColormap(name, data, N=N)
27662815
else:
2767-
data = PerceptuallyUniformColormap(name, data, N=N, **kw)
2816+
cmap = PerceptuallyUniformColormap(name, data, N=N, **kw)
27682817
if name[-2:] == '_r':
2769-
data = data.reversed(name[:-2])
2818+
cmap = cmap.reversed(name[:-2])
27702819

27712820
# Read .rgb, .rgba, .xrgb, and .xrgba files
27722821
elif ext in ('txt', 'rgb', 'xrgb', 'rgba', 'xrgba'):
@@ -2781,14 +2830,16 @@ def _load_cmap_cycle(filename, cmap=False):
27812830
except ValueError:
27822831
_warn_proplot(
27832832
f'Failed to load {filename!r}. Expected a table of comma '
2784-
'or space-separated values.')
2833+
'or space-separated values.'
2834+
)
27852835
return None, None
27862836
# Build x-coordinates and standardize shape
27872837
data = np.array(data)
27882838
if data.shape[1] != len(ext):
27892839
_warn_proplot(
27902840
f'Failed to load {filename!r}. Got {data.shape[1]} columns, '
2791-
f'but expected {len(ext)}.')
2841+
f'but expected {len(ext)}.'
2842+
)
27922843
return None, None
27932844
if ext[0] != 'x': # i.e. no x-coordinates specified explicitly
27942845
x = np.linspace(0, 1, data.shape[0])
@@ -2803,20 +2854,16 @@ def _load_cmap_cycle(filename, cmap=False):
28032854
doc = ElementTree.parse(filename)
28042855
except IOError:
28052856
_warn_proplot(f'Failed to load {filename!r}.')
2806-
return None, None
2857+
return
28072858
x, data = [], []
28082859
for s in doc.getroot().findall('.//Point'):
28092860
# Verify keys
28102861
if any(key not in s.attrib for key in 'xrgb'):
28112862
_warn_proplot(
28122863
f'Failed to load {filename!r}. Missing an x, r, g, or b '
2813-
'specification inside one or more <Point> tags.')
2814-
return None, None
2815-
if 'o' in s.attrib and 'a' in s.attrib:
2816-
_warn_proplot(
2817-
f'Failed to load {filename!r}. Contains '
2818-
'ambiguous opacity key.')
2819-
return None, None
2864+
'specification inside one or more <Point> tags.'
2865+
)
2866+
return
28202867
# Get data
28212868
color = []
28222869
for key in 'rgbao': # o for opacity
@@ -2826,11 +2873,13 @@ def _load_cmap_cycle(filename, cmap=False):
28262873
x.append(float(s.attrib['x']))
28272874
data.append(color)
28282875
# Convert to array
2829-
if not all(len(data[0]) == len(color) for color in data):
2876+
if not all(len(data[0]) == len(color)
2877+
and len(color) in (3, 4) for color in data):
28302878
_warn_proplot(
2831-
f'File {filename!r} has some points with alpha channel '
2832-
'specified, some without.')
2833-
return None, None
2879+
f'Failed to load {filename!r}. Unexpected number of channels '
2880+
'or mixed channels across <Point> tags.'
2881+
)
2882+
return
28342883

28352884
# Read hex strings
28362885
elif ext == 'hex':
@@ -2839,24 +2888,22 @@ def _load_cmap_cycle(filename, cmap=False):
28392888
data = re.findall('#[0-9a-fA-F]{6}', string) # list of strings
28402889
if len(data) < 2:
28412890
_warn_proplot(
2842-
f'Failed to load {filename!r}. Hex strings not found.')
2843-
return None, None
2891+
f'Failed to load {filename!r}. Hex strings not found.'
2892+
)
2893+
return
28442894
# Convert to array
28452895
x = np.linspace(0, 1, len(data))
28462896
data = [to_rgb(color) for color in data]
28472897
else:
28482898
_warn_proplot(
2849-
f'Colormap or cycle file {filename!r} has unknown extension.')
2850-
return None, None
2899+
f'Colormap or cycle file {filename!r} has unknown extension.'
2900+
)
2901+
return
28512902

28522903
# Standardize and reverse if necessary to cmap
28532904
# TODO: Document the fact that filenames ending in _r return a reversed
28542905
# version of the colormap stored in that file.
2855-
if isinstance(data, LinearSegmentedColormap):
2856-
if not cmap:
2857-
_warn_proplot(f'Failed to load {filename!r} as color cycle.')
2858-
return None, None
2859-
else:
2906+
if not cmap:
28602907
x, data = np.array(x), np.array(data)
28612908
# for some reason, some aren't in 0-1 range
28622909
x = (x - x.min()) / (x.max() - x.min())
@@ -2866,12 +2913,14 @@ def _load_cmap_cycle(filename, cmap=False):
28662913
name = name[:-2]
28672914
data = data[::-1, :]
28682915
x = 1 - x[::-1]
2869-
if cmap:
2916+
if listed:
2917+
cmap = ListedColormap(data, name, N=len(data))
2918+
else:
28702919
data = [(x, color) for x, color in zip(x, data)]
2871-
data = LinearSegmentedColormap.from_list(name, data, N=N)
2920+
cmap = LinearSegmentedColormap.from_list(name, data, N=N)
28722921

28732922
# Return colormap or data
2874-
return name, data
2923+
return cmap
28752924

28762925

28772926
@_timer

0 commit comments

Comments
 (0)