/
ctables.py
276 lines (210 loc) · 8.51 KB
/
ctables.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# Copyright (c) 2014,2015,2017,2019 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""Work with custom color tables.
Contains a tools for reading color tables from files, and creating instances based on a
specific set of constraints (e.g. step size) for mapping.
.. plot::
import numpy as np
import matplotlib.pyplot as plt
import metpy.plots.ctables as ctables
def plot_color_gradients(cmap_category, cmap_list, nrows):
fig, axes = plt.subplots(figsize=(7, 6), nrows=nrows)
fig.subplots_adjust(top=.93, bottom=0.01, left=0.32, right=0.99)
axes[0].set_title(cmap_category + ' colormaps', fontsize=14)
for ax, name in zip(axes, cmap_list):
ax.imshow(gradient, aspect='auto', cmap=ctables.registry.get_colortable(name))
pos = list(ax.get_position().bounds)
x_text = pos[0] - 0.01
y_text = pos[1] + pos[3]/2.
fig.text(x_text, y_text, name, va='center', ha='right', fontsize=10)
# Turn off *all* ticks & spines, not just the ones with colormaps.
for ax in axes:
ax.set_axis_off()
cmaps = list(ctables.registry)
cmaps = [name for name in cmaps if name[-2:]!='_r']
nrows = len(cmaps)
gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))
plot_color_gradients('MetPy', cmaps, nrows)
plt.show()
"""
import ast
import logging
from pathlib import Path
import matplotlib.colors as mcolors
from ..package_tools import Exporter
exporter = Exporter(globals())
TABLE_EXT = '.tbl'
log = logging.getLogger(__name__)
def _parse(s):
if hasattr(s, 'decode'):
s = s.decode('ascii')
if not s.startswith('#'):
return ast.literal_eval(s)
return None
@exporter.export
def read_colortable(fobj):
r"""Read colortable information from a file.
Reads a colortable, which consists of one color per line of the file, where
a color can be one of: a tuple of 3 floats, a string with a HTML color name,
or a string with a HTML hex color.
Parameters
----------
fobj : file-like object
A file-like object to read the colors from
Returns
-------
List of tuples
A list of the RGB color values, where each RGB color is a tuple of 3 floats in the
range of [0, 1].
"""
ret = []
try:
for line in fobj:
literal = _parse(line)
if literal:
ret.append(mcolors.colorConverter.to_rgb(literal))
return ret
except (SyntaxError, ValueError) as e:
raise RuntimeError(f'Malformed colortable (bad line: {line})') from e
def convert_gempak_table(infile, outfile):
r"""Convert a GEMPAK color table to one MetPy can read.
Reads lines from a GEMPAK-style color table file, and writes them to another file in
a format that MetPy can parse.
Parameters
----------
infile : file-like object
The file-like object to read from
outfile : file-like object
The file-like object to write to
"""
for line in infile:
if not line.startswith('!') and line.strip():
r, g, b = map(int, line.split())
outfile.write(f'({r / 255:f}, {g / 255:f}, {b / 255:f})\n')
class ColortableRegistry(dict):
r"""Manages the collection of color tables.
Provides access to color tables, read collections of files, and generates
matplotlib's Normalize instances to go with the colortable.
"""
def scan_resource(self, pkg, path):
r"""Scan a resource directory for colortable files and add them to the registry.
Parameters
----------
pkg : str
The package containing the resource directory
path : str
The path to the directory with the color tables
"""
import importlib.resources
for entry in (importlib.resources.files(pkg) / path).iterdir():
if entry.suffix == TABLE_EXT:
with entry.open() as stream:
self.add_colortable(stream, entry.with_suffix('').name)
def scan_dir(self, path):
r"""Scan a directory on disk for color table files and add them to the registry.
Parameters
----------
path : str
The path to the directory with the color tables
"""
for entry in Path(path).glob('*' + TABLE_EXT):
if entry.is_file():
with entry.open() as fobj:
try:
self.add_colortable(fobj, entry.with_suffix('').name)
log.debug('Added colortable from file: %s', entry)
except RuntimeError:
# If we get a file we can't handle, assume we weren't meant to.
log.info('Skipping unparsable file: %s', entry)
def add_colortable(self, fobj, name):
r"""Add a color table from a file to the registry.
Parameters
----------
fobj : file-like object
The file to read the color table from
name : str
The name under which the color table will be stored
"""
self[name] = read_colortable(fobj)
self[name + '_r'] = self[name][::-1]
def get_with_steps(self, name, start, step):
r"""Get a color table from the registry with a corresponding norm.
Builds a `matplotlib.colors.BoundaryNorm` using `start`, `step`, and
the number of colors, based on the color table obtained from `name`.
Parameters
----------
name : str
The name under which the color table will be stored
start : float
The starting boundary
step : float
The step between boundaries
Returns
-------
`matplotlib.colors.BoundaryNorm`, `matplotlib.colors.ListedColormap`
The boundary norm based on `start` and `step` with the number of colors
from the number of entries matching the color table, and the color table itself.
"""
from numpy import arange
# Need one more boundary than color
num_steps = len(self[name]) + 1
boundaries = arange(start, start + step * num_steps, step)
return self.get_with_boundaries(name, boundaries)
def get_with_range(self, name, start, end):
r"""Get a color table from the registry with a corresponding norm.
Builds a `matplotlib.colors.BoundaryNorm` using `start`, `end`, and
the number of colors, based on the color table obtained from `name`.
Parameters
----------
name : str
The name under which the color table will be stored
start : float
The starting boundary
end : float
The ending boundary
Returns
-------
`matplotlib.colors.BoundaryNorm`, `matplotlib.colors.ListedColormap`
The boundary norm based on `start` and `end` with the number of colors
from the number of entries matching the color table, and the color table itself.
"""
from numpy import linspace
# Need one more boundary than color
num_steps = len(self[name]) + 1
boundaries = linspace(start, end, num_steps)
return self.get_with_boundaries(name, boundaries)
def get_with_boundaries(self, name, boundaries):
r"""Get a color table from the registry with a corresponding norm.
Builds a `matplotlib.colors.BoundaryNorm` using `boundaries`.
Parameters
----------
name : str
The name under which the color table will be stored
boundaries : array-like
The list of boundaries for the norm
Returns
-------
`matplotlib.colors.BoundaryNorm`, `matplotlib.colors.ListedColormap`
The boundary norm based on `boundaries`, and the color table itself.
"""
cmap = self.get_colortable(name)
return mcolors.BoundaryNorm(boundaries, cmap.N), cmap
def get_colortable(self, name):
r"""Get a color table from the registry.
Parameters
----------
name : str
The name under which the color table will be stored
Returns
-------
`matplotlib.colors.ListedColormap`
The color table corresponding to `name`
"""
return mcolors.ListedColormap(self[name], name=name)
registry = ColortableRegistry()
registry.scan_resource('metpy.plots', 'colortable_files')
registry.scan_dir(Path.cwd())
with exporter:
colortables = registry