Completed
Pull Request — master (#482)
by
unknown
01:04
created

Hodograph.plot_layers()   A

Complexity

Conditions 1

Size

Total Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 58
rs 9.639
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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