1
|
|
|
# Copyright (c) 2008-2015 MetPy Developers. |
2
|
|
|
# Distributed under the terms of the BSD 3-Clause License. |
3
|
|
|
# SPDX-License-Identifier: BSD-3-Clause |
4
|
|
|
|
5
|
|
|
import numpy as np |
6
|
|
|
import matplotlib.transforms as transforms |
7
|
|
|
import matplotlib.axis as maxis |
8
|
|
|
import matplotlib.spines as mspines |
9
|
|
|
from matplotlib.axes import Axes |
10
|
|
|
from matplotlib.collections import LineCollection |
11
|
|
|
from matplotlib.patches import Circle |
12
|
|
|
from matplotlib.projections import register_projection |
13
|
|
|
from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter |
14
|
|
|
from .util import colored_line |
15
|
|
|
from ..calc import dry_lapse, moist_lapse, dewpoint, vapor_pressure |
16
|
|
|
from ..units import units |
17
|
|
|
|
18
|
|
|
from ..package_tools import Exporter |
19
|
|
|
|
20
|
|
|
exporter = Exporter(globals()) |
21
|
|
|
|
22
|
|
|
|
23
|
|
|
class SkewXTick(maxis.XTick): |
24
|
|
|
r""" |
25
|
|
|
This class adds to the standard :class:`matplotlib.axis.XTick` dynamic checking |
26
|
|
|
for whether a top or bottom tick is actually within the data limits at that part |
27
|
|
|
and draw as appropriate. It also performs similar checking for gridlines. |
28
|
|
|
""" |
29
|
|
|
def _need_lower(self): |
30
|
|
|
return transforms.interval_contains(self.axes.lower_xlim, self.get_loc()) |
31
|
|
|
|
32
|
|
|
def _need_upper(self): |
33
|
|
|
return transforms.interval_contains(self.axes.upper_xlim, self.get_loc()) |
34
|
|
|
|
35
|
|
|
@property |
36
|
|
|
def gridOn(self): |
37
|
|
|
return (self._gridOn and transforms.interval_contains(self.get_view_interval(), |
38
|
|
|
self.get_loc())) |
39
|
|
|
|
40
|
|
|
@gridOn.setter |
41
|
|
|
def gridOn(self, value): |
42
|
|
|
self._gridOn = value |
43
|
|
|
|
44
|
|
|
@property |
45
|
|
|
def tick1On(self): |
46
|
|
|
return self._tick1On and self._need_lower() |
47
|
|
|
|
48
|
|
|
@tick1On.setter |
49
|
|
|
def tick1On(self, value): |
50
|
|
|
self._tick1On = value |
51
|
|
|
|
52
|
|
|
@property |
53
|
|
|
def label1On(self): |
54
|
|
|
return self._label1On and self._need_lower() |
55
|
|
|
|
56
|
|
|
@label1On.setter |
57
|
|
|
def label1On(self, value): |
58
|
|
|
self._label1On = value |
59
|
|
|
|
60
|
|
|
@property |
61
|
|
|
def tick2On(self): |
62
|
|
|
return self._tick2On and self._need_upper() |
63
|
|
|
|
64
|
|
|
@tick2On.setter |
65
|
|
|
def tick2On(self, value): |
66
|
|
|
self._tick2On = value |
67
|
|
|
|
68
|
|
|
@property |
69
|
|
|
def label2On(self): |
70
|
|
|
return self._label2On and self._need_upper() |
71
|
|
|
|
72
|
|
|
@label2On.setter |
73
|
|
|
def label2On(self, value): |
74
|
|
|
self._label2On = value |
75
|
|
|
|
76
|
|
|
def get_view_interval(self): |
77
|
|
|
return self.axes.xaxis.get_view_interval() |
78
|
|
|
|
79
|
|
|
|
80
|
|
|
class SkewXAxis(maxis.XAxis): |
81
|
|
|
r""" |
82
|
|
|
This class exists to force the use of our custom :class:`SkewXTick` as well |
83
|
|
|
as provide a custom value for interview that combines the extents of the |
84
|
|
|
upper and lower x-limits from the axes. |
85
|
|
|
""" |
86
|
|
|
def _get_tick(self, major): |
87
|
|
|
return SkewXTick(self.axes, 0, '', major=major) |
88
|
|
|
|
89
|
|
|
def get_view_interval(self): |
90
|
|
|
return self.axes.upper_xlim[0], self.axes.lower_xlim[1] |
91
|
|
|
|
92
|
|
|
|
93
|
|
|
class SkewSpine(mspines.Spine): |
94
|
|
|
r""" |
95
|
|
|
This class exists to use the separate x-limits from the axes to properly |
96
|
|
|
locate the spine. |
97
|
|
|
""" |
98
|
|
|
def _adjust_location(self): |
99
|
|
|
pts = self._path.vertices |
100
|
|
|
if self.spine_type == 'top': |
101
|
|
|
pts[:, 0] = self.axes.upper_xlim |
102
|
|
|
else: |
103
|
|
|
pts[:, 0] = self.axes.lower_xlim |
104
|
|
|
|
105
|
|
|
|
106
|
|
|
class SkewXAxes(Axes): |
107
|
|
|
r""" |
108
|
|
|
This class handles registration of the skew-xaxes as a projection as well as setting up |
109
|
|
|
the appropriate transformations. It also makes sure we use our instances for spines |
110
|
|
|
and x-axis: :class:`SkewSpine` and :class:`SkewXAxis`. It provides properties to |
111
|
|
|
facilitate finding the x-limits for the bottom and top of the plot as well. |
112
|
|
|
""" |
113
|
|
|
# The projection must specify a name. This will be used be the |
114
|
|
|
# user to select the projection, i.e. ``subplot(111, |
115
|
|
|
# projection='skewx')``. |
116
|
|
|
name = 'skewx' |
117
|
|
|
|
118
|
|
|
def __init__(self, *args, **kwargs): |
119
|
|
|
# This needs to be popped and set before moving on |
120
|
|
|
self.rot = kwargs.pop('rotation', 30) |
121
|
|
|
Axes.__init__(self, *args, **kwargs) |
122
|
|
|
|
123
|
|
|
def _init_axis(self): |
124
|
|
|
# Taken from Axes and modified to use our modified X-axis |
125
|
|
|
self.xaxis = SkewXAxis(self) |
126
|
|
|
self.spines['top'].register_axis(self.xaxis) |
127
|
|
|
self.spines['bottom'].register_axis(self.xaxis) |
128
|
|
|
self.yaxis = maxis.YAxis(self) |
129
|
|
|
self.spines['left'].register_axis(self.yaxis) |
130
|
|
|
self.spines['right'].register_axis(self.yaxis) |
131
|
|
|
|
132
|
|
|
def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): |
133
|
|
|
# pylint: disable=unused-argument |
134
|
|
|
spines = {'top': SkewSpine.linear_spine(self, 'top'), |
135
|
|
|
'bottom': mspines.Spine.linear_spine(self, 'bottom'), |
136
|
|
|
'left': mspines.Spine.linear_spine(self, 'left'), |
137
|
|
|
'right': mspines.Spine.linear_spine(self, 'right')} |
138
|
|
|
return spines |
139
|
|
|
|
140
|
|
|
def _set_lim_and_transforms(self): |
141
|
|
|
""" |
142
|
|
|
This is called once when the plot is created to set up all the |
143
|
|
|
transforms for the data, text and grids. |
144
|
|
|
""" |
145
|
|
|
# Get the standard transform setup from the Axes base class |
146
|
|
|
Axes._set_lim_and_transforms(self) |
147
|
|
|
|
148
|
|
|
# Need to put the skew in the middle, after the scale and limits, |
149
|
|
|
# but before the transAxes. This way, the skew is done in Axes |
150
|
|
|
# coordinates thus performing the transform around the proper origin |
151
|
|
|
# We keep the pre-transAxes transform around for other users, like the |
152
|
|
|
# spines for finding bounds |
153
|
|
|
self.transDataToAxes = (self.transScale + |
154
|
|
|
(self.transLimits + |
155
|
|
|
transforms.Affine2D().skew_deg(self.rot, 0))) |
156
|
|
|
|
157
|
|
|
# Create the full transform from Data to Pixels |
158
|
|
|
self.transData = self.transDataToAxes + self.transAxes |
159
|
|
|
|
160
|
|
|
# Blended transforms like this need to have the skewing applied using |
161
|
|
|
# both axes, in axes coords like before. |
162
|
|
|
self._xaxis_transform = (transforms.blended_transform_factory( |
163
|
|
|
self.transScale + self.transLimits, |
164
|
|
|
transforms.IdentityTransform()) + |
165
|
|
|
transforms.Affine2D().skew_deg(self.rot, 0)) + self.transAxes |
166
|
|
|
|
167
|
|
|
@property |
168
|
|
|
def lower_xlim(self): |
169
|
|
|
r"The data limits for the x-axis along the bottom of the axes" |
170
|
|
|
return self.axes.viewLim.intervalx |
171
|
|
|
|
172
|
|
|
@property |
173
|
|
|
def upper_xlim(self): |
174
|
|
|
r"The data limits for the x-axis along the top of the axes" |
175
|
|
|
return self.transDataToAxes.inverted().transform([[0., 1.], [1., 1.]])[:, 0] |
176
|
|
|
|
177
|
|
|
|
178
|
|
|
# Now register the projection with matplotlib so the user can select |
179
|
|
|
# it. |
180
|
|
|
register_projection(SkewXAxes) |
181
|
|
|
|
182
|
|
|
|
183
|
|
|
@exporter.export |
184
|
|
|
class SkewT(object): |
185
|
|
|
r'''Make Skew-T log-P plots of data |
186
|
|
|
|
187
|
|
|
This class simplifies the process of creating Skew-T log-P plots in |
188
|
|
|
using matplotlib. It handles requesting the appropriate skewed projection, |
189
|
|
|
and provides simplified wrappers to make it easy to plot data, add wind |
190
|
|
|
barbs, and add other lines to the plots (e.g. dry adiabats) |
191
|
|
|
|
192
|
|
|
Attributes |
193
|
|
|
---------- |
194
|
|
|
ax : `matplotlib.axes.Axes` |
195
|
|
|
The underlying Axes instance, which can be used for calling additional |
196
|
|
|
plot functions (e.g. `axvline`) |
197
|
|
|
''' |
198
|
|
|
|
199
|
|
|
def __init__(self, fig=None, rotation=30, subplot=(1, 1, 1)): |
200
|
|
|
r'''Creates SkewT - logP plots. |
201
|
|
|
|
202
|
|
|
Parameters |
203
|
|
|
---------- |
204
|
|
|
fig : matplotlib.figure.Figure, optional |
205
|
|
|
Source figure to use for plotting. If none is given, a new |
206
|
|
|
:class:`matplotlib.figure.Figure` instance will be created. |
207
|
|
|
rotation : float or int, optional |
208
|
|
|
Controls the rotation of temperature relative to horizontal. Given |
209
|
|
|
in degrees counterclockwise from x-axis. Defaults to 30 degrees. |
210
|
|
|
subplot : tuple[int, int, int] or `matplotlib.gridspec.SubplotSpec` instance, optional |
211
|
|
|
Controls the size/position of the created subplot. This allows creating |
212
|
|
|
the skewT as part of a collection of subplots. If subplot is a tuple, it |
213
|
|
|
should conform to the specification used for |
214
|
|
|
:meth:`matplotlib.figure.Figure.add_subplot`. The |
215
|
|
|
:class:`matplotlib.gridspec.SubplotSpec` |
216
|
|
|
can be created by using :class:`matplotlib.gridspec.GridSpec`. |
217
|
|
|
''' |
218
|
|
|
|
219
|
|
|
if fig is None: |
220
|
|
|
import matplotlib.pyplot as plt |
221
|
|
|
figsize = plt.rcParams.get('figure.figsize', (7, 7)) |
222
|
|
|
fig = plt.figure(figsize=figsize) |
223
|
|
|
self._fig = fig |
224
|
|
|
|
225
|
|
|
# Handle being passed a tuple for the subplot, or a GridSpec instance |
226
|
|
|
try: |
227
|
|
|
len(subplot) |
228
|
|
|
except TypeError: |
229
|
|
|
subplot = (subplot,) |
230
|
|
|
self.ax = fig.add_subplot(*subplot, projection='skewx', rotation=rotation) |
231
|
|
|
self.ax.grid(True) |
232
|
|
|
|
233
|
|
|
def plot(self, p, t, *args, **kwargs): |
234
|
|
|
r'''Plot data. |
235
|
|
|
|
236
|
|
|
Simple wrapper around plot so that pressure is the first (independent) |
237
|
|
|
input. This is essentially a wrapper around `semilogy`. It also |
238
|
|
|
sets some appropriate ticking and plot ranges. |
239
|
|
|
|
240
|
|
|
Parameters |
241
|
|
|
---------- |
242
|
|
|
p : array_like |
243
|
|
|
pressure values |
244
|
|
|
t : array_like |
245
|
|
|
temperature values, can also be used for things like dew point |
246
|
|
|
args |
247
|
|
|
Other positional arguments to pass to :func:`~matplotlib.pyplot.semilogy` |
248
|
|
|
kwargs |
249
|
|
|
Other keyword arguments to pass to :func:`~matplotlib.pyplot.semilogy` |
250
|
|
|
|
251
|
|
|
Returns |
252
|
|
|
------- |
253
|
|
|
list[matplotlib.lines.Line2D] |
254
|
|
|
lines plotted |
255
|
|
|
|
256
|
|
|
See Also |
257
|
|
|
-------- |
258
|
|
|
:func:`matplotlib.pyplot.semilogy` |
259
|
|
|
''' |
260
|
|
|
|
261
|
|
|
# Skew-T logP plotting |
262
|
|
|
l = self.ax.semilogy(t, p, *args, **kwargs) |
263
|
|
|
|
264
|
|
|
# Disables the log-formatting that comes with semilogy |
265
|
|
|
self.ax.yaxis.set_major_formatter(ScalarFormatter()) |
266
|
|
|
self.ax.yaxis.set_major_locator(MultipleLocator(100)) |
267
|
|
|
self.ax.yaxis.set_minor_formatter(NullFormatter()) |
268
|
|
|
if not self.ax.yaxis_inverted(): |
269
|
|
|
self.ax.invert_yaxis() |
270
|
|
|
|
271
|
|
|
# Try to make sane default temperature plotting |
272
|
|
|
self.ax.xaxis.set_major_locator(MultipleLocator(10)) |
273
|
|
|
self.ax.set_xlim(-50, 50) |
274
|
|
|
|
275
|
|
|
return l |
276
|
|
|
|
277
|
|
|
def plot_barbs(self, p, u, v, xloc=1.0, x_clip_radius=0.08, y_clip_radius=0.08, **kwargs): |
278
|
|
|
r'''Plot wind barbs. |
279
|
|
|
|
280
|
|
|
Adds wind barbs to the skew-T plot. This is a wrapper around the |
281
|
|
|
`barbs` command that adds to appropriate transform to place the |
282
|
|
|
barbs in a vertical line, located as a function of pressure. |
283
|
|
|
|
284
|
|
|
Parameters |
285
|
|
|
---------- |
286
|
|
|
p : array_like |
287
|
|
|
pressure values |
288
|
|
|
u : array_like |
289
|
|
|
U (East-West) component of wind |
290
|
|
|
v : array_like |
291
|
|
|
V (North-South) component of wind |
292
|
|
|
xloc : float, optional |
293
|
|
|
Position for the barbs, in normalized axes coordinates, where 0.0 |
294
|
|
|
denotes far left and 1.0 denotes far right. Defaults to far right. |
295
|
|
|
x_clip_radius : float, optional |
296
|
|
|
Space, in normalized axes coordinates, to leave before clipping |
297
|
|
|
wind barbs in the x-direction. Defaults to 0.08. |
298
|
|
|
y_clip_radius : float, optional |
299
|
|
View Code Duplication |
Space, in normalized axes coordinates, to leave above/below plot |
|
|
|
|
300
|
|
|
before clipping wind barbs in the y-direction. Defaults to 0.08. |
301
|
|
|
kwargs |
302
|
|
|
Other keyword arguments to pass to :func:`~matplotlib.pyplot.barbs` |
303
|
|
|
|
304
|
|
|
Returns |
305
|
|
|
------- |
306
|
|
|
matplotlib.quiver.Barbs |
307
|
|
|
instance created |
308
|
|
|
|
309
|
|
|
See Also |
310
|
|
|
-------- |
311
|
|
|
:func:`matplotlib.pyplot.barbs` |
312
|
|
|
''' |
313
|
|
|
|
314
|
|
|
# Assemble array of x-locations in axes space |
315
|
|
|
x = np.empty_like(p) |
316
|
|
|
x.fill(xloc) |
317
|
|
|
|
318
|
|
|
# Do barbs plot at this location |
319
|
|
|
b = self.ax.barbs(x, p, u, v, |
320
|
|
|
transform=self.ax.get_yaxis_transform(which='tick2'), |
321
|
|
|
clip_on=True, **kwargs) |
322
|
|
|
|
323
|
|
|
# Override the default clip box, which is the axes rectangle, so we can have |
324
|
|
|
# barbs that extend outside. |
325
|
|
|
ax_bbox = transforms.Bbox([[xloc - x_clip_radius, -y_clip_radius], |
326
|
|
|
[xloc + x_clip_radius, 1.0 + y_clip_radius]]) |
327
|
|
|
b.set_clip_box(transforms.TransformedBbox(ax_bbox, self.ax.transAxes)) |
328
|
|
|
return b |
329
|
|
|
|
330
|
|
|
def plot_dry_adiabats(self, t0=None, p=None, **kwargs): |
331
|
|
|
r'''Plot dry adiabats. |
332
|
|
|
|
333
|
|
|
Adds dry adiabats (lines of constant potential temperature) to the |
334
|
|
|
plot. The default style of these lines is dashed red lines with an alpha |
335
|
|
|
value of 0.5. These can be overridden using keyword arguments. |
336
|
|
|
|
337
|
|
|
Parameters |
338
|
|
|
---------- |
339
|
|
|
t0 : array_like, optional |
340
|
|
|
Starting temperature values in Kelvin. If none are given, they will be |
341
|
|
|
generated using the current temperature range at the bottom of |
342
|
|
|
the plot. |
343
|
|
|
p : array_like, optional |
344
|
|
|
Pressure values to be included in the dry adiabats. If not |
345
|
|
|
specified, they will be linearly distributed across the current |
346
|
|
|
plotted pressure range. |
347
|
|
|
kwargs |
348
|
|
|
Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` |
349
|
|
|
|
350
|
|
View Code Duplication |
Returns |
|
|
|
|
351
|
|
|
------- |
352
|
|
|
matplotlib.collections.LineCollection |
353
|
|
|
instance created |
354
|
|
|
|
355
|
|
|
See Also |
356
|
|
|
-------- |
357
|
|
|
:func:`~metpy.calc.thermo.dry_lapse` |
358
|
|
|
:meth:`plot_moist_adiabats` |
359
|
|
|
:class:`matplotlib.collections.LineCollection` |
360
|
|
|
''' |
361
|
|
|
|
362
|
|
|
# Determine set of starting temps if necessary |
363
|
|
|
if t0 is None: |
364
|
|
|
xmin, xmax = self.ax.get_xlim() |
365
|
|
|
t0 = np.arange(xmin, xmax + 1, 10) * units.degC |
366
|
|
|
|
367
|
|
|
# Get pressure levels based on ylims if necessary |
368
|
|
|
if p is None: |
369
|
|
|
p = np.linspace(*self.ax.get_ylim()) * units.mbar |
370
|
|
|
|
371
|
|
|
# Assemble into data for plotting |
372
|
|
|
t = dry_lapse(p, t0[:, np.newaxis]).to(units.degC) |
373
|
|
|
linedata = [np.vstack((ti, p)).T for ti in t] |
374
|
|
|
|
375
|
|
|
# Add to plot |
376
|
|
|
kwargs.setdefault('colors', 'r') |
377
|
|
|
kwargs.setdefault('linestyles', 'dashed') |
378
|
|
|
kwargs.setdefault('alpha', 0.5) |
379
|
|
|
return self.ax.add_collection(LineCollection(linedata, **kwargs)) |
380
|
|
|
|
381
|
|
|
def plot_moist_adiabats(self, t0=None, p=None, **kwargs): |
382
|
|
|
r'''Plot moist adiabats. |
383
|
|
|
|
384
|
|
|
Adds saturated pseudo-adiabats (lines of constant equivalent potential |
385
|
|
|
temperature) to the plot. The default style of these lines is dashed |
386
|
|
|
blue lines with an alpha value of 0.5. These can be overridden using |
387
|
|
|
keyword arguments. |
388
|
|
|
|
389
|
|
|
Parameters |
390
|
|
|
---------- |
391
|
|
|
t0 : array_like, optional |
392
|
|
|
Starting temperature values in Kelvin. If none are given, they will be |
393
|
|
|
generated using the current temperature range at the bottom of |
394
|
|
|
the plot. |
395
|
|
|
p : array_like, optional |
396
|
|
|
Pressure values to be included in the moist adiabats. If not |
397
|
|
|
specified, they will be linearly distributed across the current |
398
|
|
|
plotted pressure range. |
399
|
|
|
kwargs |
400
|
|
|
Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` |
401
|
|
|
|
402
|
|
|
Returns |
403
|
|
|
------- |
404
|
|
|
matplotlib.collections.LineCollection |
405
|
|
|
instance created |
406
|
|
|
|
407
|
|
|
See Also |
408
|
|
|
-------- |
409
|
|
|
:func:`~metpy.calc.thermo.moist_lapse` |
410
|
|
|
:meth:`plot_dry_adiabats` |
411
|
|
|
:class:`matplotlib.collections.LineCollection` |
412
|
|
|
''' |
413
|
|
|
|
414
|
|
|
# Determine set of starting temps if necessary |
415
|
|
|
if t0 is None: |
416
|
|
|
xmin, xmax = self.ax.get_xlim() |
417
|
|
|
t0 = np.concatenate((np.arange(xmin, 0, 10), |
418
|
|
|
np.arange(0, xmax + 1, 5))) * units.degC |
419
|
|
|
|
420
|
|
|
# Get pressure levels based on ylims if necessary |
421
|
|
|
if p is None: |
422
|
|
|
p = np.linspace(*self.ax.get_ylim()) * units.mbar |
423
|
|
|
|
424
|
|
|
# Assemble into data for plotting |
425
|
|
|
t = moist_lapse(p, t0[:, np.newaxis]).to(units.degC) |
426
|
|
|
linedata = [np.vstack((ti, p)).T for ti in t] |
427
|
|
|
|
428
|
|
|
# Add to plot |
429
|
|
|
kwargs.setdefault('colors', 'b') |
430
|
|
|
kwargs.setdefault('linestyles', 'dashed') |
431
|
|
|
kwargs.setdefault('alpha', 0.5) |
432
|
|
|
return self.ax.add_collection(LineCollection(linedata, **kwargs)) |
433
|
|
|
|
434
|
|
|
def plot_mixing_lines(self, w=None, p=None, **kwargs): |
435
|
|
|
r'''Plot lines of constant mixing ratio. |
436
|
|
|
|
437
|
|
|
Adds lines of constant mixing ratio (isohumes) to the |
438
|
|
|
plot. The default style of these lines is dashed green lines with an |
439
|
|
|
alpha value of 0.8. These can be overridden using keyword arguments. |
440
|
|
|
|
441
|
|
|
Parameters |
442
|
|
|
---------- |
443
|
|
|
w : array_like, optional |
444
|
|
|
Unitless mixing ratio values to plot. If none are given, default |
445
|
|
|
values are used. |
446
|
|
|
p : array_like, optional |
447
|
|
|
Pressure values to be included in the isohumes. If not |
448
|
|
|
specified, they will be linearly distributed across the current |
449
|
|
|
plotted pressure range up to 600 mb. |
450
|
|
|
kwargs |
451
|
|
|
Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` |
452
|
|
|
|
453
|
|
|
Returns |
454
|
|
|
------- |
455
|
|
|
matplotlib.collections.LineCollection |
456
|
|
|
instance created |
457
|
|
|
|
458
|
|
|
See Also |
459
|
|
|
-------- |
460
|
|
|
:class:`matplotlib.collections.LineCollection` |
461
|
|
|
''' |
462
|
|
|
|
463
|
|
|
# Default mixing level values if necessary |
464
|
|
|
if w is None: |
465
|
|
|
w = np.array([0.0004, 0.001, 0.002, 0.004, 0.007, 0.01, |
466
|
|
|
0.016, 0.024, 0.032]).reshape(-1, 1) |
467
|
|
|
|
468
|
|
|
# Set pressure range if necessary |
469
|
|
|
if p is None: |
470
|
|
|
p = np.linspace(600, max(self.ax.get_ylim())) * units.mbar |
471
|
|
|
|
472
|
|
|
# Assemble data for plotting |
473
|
|
|
td = dewpoint(vapor_pressure(p, w)) |
474
|
|
|
linedata = [np.vstack((t, p)).T for t in td] |
475
|
|
|
|
476
|
|
|
# Add to plot |
477
|
|
|
kwargs.setdefault('colors', 'g') |
478
|
|
|
kwargs.setdefault('linestyles', 'dashed') |
479
|
|
|
kwargs.setdefault('alpha', 0.8) |
480
|
|
|
return self.ax.add_collection(LineCollection(linedata, **kwargs)) |
481
|
|
|
|
482
|
|
|
|
483
|
|
|
@exporter.export |
484
|
|
|
class Hodograph(object): |
485
|
|
|
r'''Make a hodograph of wind data--plots the u and v components of the wind along the |
486
|
|
|
x and y axes, respectively. |
487
|
|
|
|
488
|
|
|
This class simplifies the process of creating a hodograph using matplotlib. |
489
|
|
|
It provides helpers for creating a circular grid and for plotting the wind as a line |
490
|
|
|
colored by another value (such as wind speed). |
491
|
|
|
|
492
|
|
|
Attributes |
493
|
|
|
---------- |
494
|
|
|
ax : `matplotlib.axes.Axes` |
495
|
|
|
The underlying Axes instance used for all plotting |
496
|
|
|
''' |
497
|
|
|
def __init__(self, ax=None, component_range=80): |
498
|
|
|
r'''Create a Hodograph instance. |
499
|
|
|
|
500
|
|
|
Parameters |
501
|
|
|
---------- |
502
|
|
|
ax : `matplotlib.axes.Axes`, optional |
503
|
|
|
The `Axes` instance used for plotting |
504
|
|
|
component_range : value |
505
|
|
|
The maximum range of the plot. Used to set plot bounds and control the maximum |
506
|
|
|
number of grid rings needed. |
507
|
|
|
''' |
508
|
|
|
if ax is None: |
509
|
|
|
import matplotlib.pyplot as plt |
510
|
|
|
self.ax = plt.figure().add_subplot(1, 1, 1) |
511
|
|
|
else: |
512
|
|
|
self.ax = ax |
513
|
|
|
ax.set_aspect('equal', 'box') |
514
|
|
|
ax.set_xlim(-component_range, component_range) |
515
|
|
|
ax.set_ylim(-component_range, component_range) |
516
|
|
|
|
517
|
|
|
# == sqrt(2) * max_range, which is the distance at the corner |
518
|
|
|
self.max_range = 1.4142135 * component_range |
519
|
|
|
|
520
|
|
|
def add_grid(self, increment=10., **kwargs): |
521
|
|
|
r'''Add grid lines to hodograph. |
522
|
|
|
|
523
|
|
|
Creates lines for the x- and y-axes, as well as circles denoting wind speed values. |
524
|
|
|
|
525
|
|
|
Parameters |
526
|
|
|
---------- |
527
|
|
|
increment : value, optional |
528
|
|
|
The value increment between rings |
529
|
|
|
kwargs |
530
|
|
|
Other kwargs to control appearance of lines |
531
|
|
|
|
532
|
|
|
See Also |
533
|
|
|
-------- |
534
|
|
|
:class:`matplotlib.patches.Circle` |
535
|
|
|
:meth:`matplotlib.axes.Axes.axhline` |
536
|
|
|
:meth:`matplotlib.axes.Axes.axvline` |
537
|
|
|
''' |
538
|
|
|
# Some default arguments. Take those, and update with any |
539
|
|
|
# arguments passed in |
540
|
|
|
grid_args = dict(color='grey', linestyle='dashed') |
541
|
|
|
if kwargs: |
542
|
|
|
grid_args.update(kwargs) |
543
|
|
|
|
544
|
|
|
# Take those args and make appropriate for a Circle |
545
|
|
|
circle_args = grid_args.copy() |
546
|
|
|
color = circle_args.pop('color', None) |
547
|
|
|
circle_args['edgecolor'] = color |
548
|
|
|
circle_args['fill'] = False |
549
|
|
|
|
550
|
|
|
self.rings = [] |
551
|
|
|
for r in np.arange(increment, self.max_range, increment): |
552
|
|
|
c = Circle((0, 0), radius=r, **circle_args) |
553
|
|
|
self.ax.add_patch(c) |
554
|
|
|
self.rings.append(c) |
555
|
|
|
|
556
|
|
|
# Add lines for x=0 and y=0 |
557
|
|
|
self.yaxis = self.ax.axvline(0, **grid_args) |
558
|
|
|
self.xaxis = self.ax.axhline(0, **grid_args) |
559
|
|
|
|
560
|
|
|
@staticmethod |
561
|
|
|
def _form_line_args(kwargs): |
562
|
|
|
r'Simple helper to take default line style and extend with kwargs' |
563
|
|
|
def_args = dict(linewidth=3) |
564
|
|
|
def_args.update(kwargs) |
565
|
|
|
return def_args |
566
|
|
|
|
567
|
|
|
def plot(self, u, v, **kwargs): |
568
|
|
|
r'''Plot u, v data. |
569
|
|
|
|
570
|
|
|
Plots the wind data on the hodograph. |
571
|
|
|
|
572
|
|
|
Parameters |
573
|
|
|
---------- |
574
|
|
|
u : array_like |
575
|
|
|
u-component of wind |
576
|
|
|
v : array_like |
577
|
|
|
v-component of wind |
578
|
|
|
kwargs |
579
|
|
|
Other keyword arguments to pass to :meth:`matplotlib.axes.Axes.plot` |
580
|
|
|
|
581
|
|
|
Returns |
582
|
|
|
------- |
583
|
|
|
list[matplotlib.lines.Line2D] |
584
|
|
|
lines plotted |
585
|
|
|
|
586
|
|
|
See Also |
587
|
|
|
-------- |
588
|
|
|
:meth:`Hodograph.plot_colormapped` |
589
|
|
|
''' |
590
|
|
|
line_args = self._form_line_args(kwargs) |
591
|
|
|
return self.ax.plot(u, v, **line_args) |
592
|
|
|
|
593
|
|
|
def plot_colormapped(self, u, v, c, **kwargs): |
594
|
|
|
r'''Plot u, v data, with line colored based on a third set of data. |
595
|
|
|
|
596
|
|
|
Plots the wind data on the hodograph, but |
597
|
|
|
|
598
|
|
|
Simple wrapper around plot so that pressure is the first (independent) |
599
|
|
|
input. This is essentially a wrapper around `semilogy`. It also |
600
|
|
|
sets some appropriate ticking and plot ranges. |
601
|
|
|
|
602
|
|
|
Parameters |
603
|
|
|
---------- |
604
|
|
|
u : array_like |
605
|
|
|
u-component of wind |
606
|
|
|
v : array_like |
607
|
|
|
v-component of wind |
608
|
|
|
c : array_like |
609
|
|
|
data to use for colormapping |
610
|
|
|
kwargs |
611
|
|
|
Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` |
612
|
|
|
|
613
|
|
|
Returns |
614
|
|
|
------- |
615
|
|
|
matplotlib.collections.LineCollection |
616
|
|
|
instance created |
617
|
|
|
|
618
|
|
|
See Also |
619
|
|
|
-------- |
620
|
|
|
:meth:`Hodograph.plot` |
621
|
|
|
''' |
622
|
|
|
line_args = self._form_line_args(kwargs) |
623
|
|
|
lc = colored_line(u, v, c, **line_args) |
624
|
|
|
self.ax.add_collection(lc) |
625
|
|
|
return lc |
626
|
|
|
|