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

Hodograph.plot_layer()   B

Complexity

Conditions 2

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

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