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