1# Copyright (c) 2014,2015,2016,2017,2019 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
6Contain 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
10from contextlib import ExitStack
11import warnings
12
13import matplotlib
14from matplotlib.axes import Axes
15import matplotlib.axis as maxis
16from matplotlib.collections import LineCollection
17import matplotlib.colors as mcolors
18from matplotlib.patches import Circle
19from matplotlib.projections import register_projection
20import matplotlib.spines as mspines
21from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter
22import matplotlib.transforms as transforms
23import numpy as np
24
25from ._util import colored_line
26from ..calc import dewpoint, dry_lapse, el, lcl, moist_lapse, vapor_pressure
27from ..calc.tools import _delete_masked_points
28from ..interpolate import interpolate_1d
29from ..package_tools import Exporter
30from ..units import concatenate, units
31
32exporter = Exporter(globals())
33
34
35class SkewTTransform(transforms.Affine2D):
36    """Perform Skew transform for Skew-T plotting.
37
38    This works in pixel space, so is designed to be applied after the normal plotting
39    transformations.
40    """
41
42    def __init__(self, bbox, rot):
43        """Initialize skew transform.
44
45        This needs a reference to the parent bounding box to do the appropriate math and
46        to register it as a child so that the transform is invalidated and regenerated if
47        the bounding box changes.
48        """
49        super().__init__()
50        self._bbox = bbox
51        self.set_children(bbox)
52        self.invalidate()
53
54        # We're not trying to support changing the rotation, so go ahead and convert to
55        # the right factor for skewing here and just save that.
56        self._rot_factor = np.tan(np.deg2rad(rot))
57
58    def get_matrix(self):
59        """Return transformation matrix."""
60        if self._invalid:
61            # The following matrix is equivalent to the following:
62            # x0, y0 = self._bbox.xmin, self._bbox.ymin
63            # self.translate(-x0, -y0).skew_deg(self._rot, 0).translate(x0, y0)
64            # Setting it this way is just more efficient.
65            self._mtx = np.array([[1.0, self._rot_factor, -self._rot_factor * self._bbox.ymin],
66                                  [0.0, 1.0, 0.0],
67                                  [0.0, 0.0, 1.0]])
68
69            # Need to clear both the invalid flag *and* reset the inverse, which is cached
70            # by the parent class.
71            self._invalid = 0
72            self._inverted = None
73        return self._mtx
74
75
76class SkewXTick(maxis.XTick):
77    r"""Make x-axis ticks for Skew-T plots.
78
79    This class adds to the standard :class:`matplotlib.axis.XTick` dynamic checking
80    for whether a top or bottom tick is actually within the data limits at that part
81    and draw as appropriate. It also performs similar checking for gridlines.
82    """
83
84    # Taken from matplotlib's SkewT example to update for matplotlib 3.1's changes to
85    # state management for ticks. See matplotlib/matplotlib#10088
86    def draw(self, renderer):
87        """Draw the tick."""
88        # When adding the callbacks with `stack.callback`, we fetch the current
89        # visibility state of the artist with `get_visible`; the ExitStack will
90        # restore these states (`set_visible`) at the end of the block (after
91        # the draw).
92        with ExitStack() as stack:
93            for artist in [self.gridline, self.tick1line, self.tick2line,
94                           self.label1, self.label2]:
95                stack.callback(artist.set_visible, artist.get_visible())
96
97            self.tick1line.set_visible(self.tick1line.get_visible() and self.lower_in_bounds)
98            self.label1.set_visible(self.label1.get_visible() and self.lower_in_bounds)
99            self.tick2line.set_visible(self.tick2line.get_visible() and self.upper_in_bounds)
100            self.label2.set_visible(self.label2.get_visible() and self.upper_in_bounds)
101            self.gridline.set_visible(self.gridline.get_visible() and self.grid_in_bounds)
102            super().draw(renderer)
103
104    @property
105    def lower_in_bounds(self):
106        """Whether the lower part of the tick is in bounds."""
107        return transforms.interval_contains(self.axes.lower_xlim, self.get_loc())
108
109    @property
110    def upper_in_bounds(self):
111        """Whether the upper part of the tick is in bounds."""
112        return transforms.interval_contains(self.axes.upper_xlim, self.get_loc())
113
114    @property
115    def grid_in_bounds(self):
116        """Whether any of the tick grid line is in bounds."""
117        return transforms.interval_contains(self.axes.xaxis.get_view_interval(),
118                                            self.get_loc())
119
120
121class SkewXAxis(maxis.XAxis):
122    r"""Make an x-axis that works properly for Skew-T plots.
123
124    This class exists to force the use of our custom :class:`SkewXTick` as well
125    as provide a custom value for interval that combines the extents of the
126    upper and lower x-limits from the axes.
127    """
128
129    def _get_tick(self, major):
130        # Warning stuff can go away when we only support Matplotlib >=3.3
131        with warnings.catch_warnings():
132            warnings.simplefilter('ignore', getattr(
133                matplotlib, 'MatplotlibDeprecationWarning', DeprecationWarning))
134            return SkewXTick(self.axes, None, label=None, major=major)
135
136    # Needed to properly handle tight bbox
137    def _get_tick_bboxes(self, ticks, renderer):
138        """Return lists of bboxes for ticks' label1's and label2's."""
139        return ([tick.label1.get_window_extent(renderer)
140                 for tick in ticks if tick.label1.get_visible() and tick.lower_in_bounds],
141                [tick.label2.get_window_extent(renderer)
142                 for tick in ticks if tick.label2.get_visible() and tick.upper_in_bounds])
143
144    def get_view_interval(self):
145        """Get the view interval."""
146        return self.axes.upper_xlim[0], self.axes.lower_xlim[1]
147
148
149class SkewSpine(mspines.Spine):
150    r"""Make an x-axis spine that works properly for Skew-T plots.
151
152    This class exists to use the separate x-limits from the axes to properly
153    locate the spine.
154    """
155
156    def _adjust_location(self):
157        pts = self._path.vertices
158        if self.spine_type == 'top':
159            pts[:, 0] = self.axes.upper_xlim
160        else:
161            pts[:, 0] = self.axes.lower_xlim
162
163
164class SkewXAxes(Axes):
165    r"""Make a set of axes for Skew-T plots.
166
167    This class handles registration of the skew-xaxes as a projection as well as setting up
168    the appropriate transformations. It also makes sure we use our instances for spines
169    and x-axis: :class:`SkewSpine` and :class:`SkewXAxis`. It provides properties to
170    facilitate finding the x-limits for the bottom and top of the plot as well.
171    """
172
173    # The projection must specify a name.  This will be used be the
174    # user to select the projection, i.e. ``subplot(111,
175    # projection='skewx')``.
176    name = 'skewx'
177
178    def __init__(self, *args, **kwargs):
179        r"""Initialize `SkewXAxes`.
180
181        Parameters
182        ----------
183        args : Arbitrary positional arguments
184            Passed to :class:`matplotlib.axes.Axes`
185
186        position: int, optional
187            The rotation of the x-axis against the y-axis, in degrees.
188
189        kwargs : Arbitrary keyword arguments
190            Passed to :class:`matplotlib.axes.Axes`
191
192        """
193        # This needs to be popped and set before moving on
194        self.rot = kwargs.pop('rotation', 30)
195        super().__init__(*args, **kwargs)
196
197    def _init_axis(self):
198        # Taken from Axes and modified to use our modified X-axis
199        self.xaxis = SkewXAxis(self)
200        self.spines['top'].register_axis(self.xaxis)
201        self.spines['bottom'].register_axis(self.xaxis)
202        self.yaxis = maxis.YAxis(self)
203        self.spines['left'].register_axis(self.yaxis)
204        self.spines['right'].register_axis(self.yaxis)
205
206    def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'):
207        # pylint: disable=unused-argument
208        spines = {'top': SkewSpine.linear_spine(self, 'top'),
209                  'bottom': mspines.Spine.linear_spine(self, 'bottom'),
210                  'left': mspines.Spine.linear_spine(self, 'left'),
211                  'right': mspines.Spine.linear_spine(self, 'right')}
212        return spines
213
214    def _set_lim_and_transforms(self):
215        """Set limits and transforms.
216
217        This is called once when the plot is created to set up all the
218        transforms for the data, text and grids.
219
220        """
221        # Get the standard transform setup from the Axes base class
222        super()._set_lim_and_transforms()
223
224        # This transformation handles the skewing
225        skew_trans = SkewTTransform(self.bbox, self.rot)
226
227        # Create the full transform from Data to Pixels
228        self.transData += skew_trans
229
230        # Blended transforms like this need to have the skewing applied using
231        # both axes, in axes coords like before.
232        self._xaxis_transform += skew_trans
233
234    @property
235    def lower_xlim(self):
236        """Get the data limits for the x-axis along the bottom of the axes."""
237        return self.axes.viewLim.intervalx
238
239    @property
240    def upper_xlim(self):
241        """Get the data limits for the x-axis along the top of the axes."""
242        return self.transData.inverted().transform([[self.bbox.xmin, self.bbox.ymax],
243                                                    self.bbox.max])[:, 0]
244
245
246# Now register the projection with matplotlib so the user can select it.
247register_projection(SkewXAxes)
248
249
250@exporter.export
251class SkewT:
252    r"""Make Skew-T log-P plots of data.
253
254    This class simplifies the process of creating Skew-T log-P plots in
255    using matplotlib. It handles requesting the appropriate skewed projection,
256    and provides simplified wrappers to make it easy to plot data, add wind
257    barbs, and add other lines to the plots (e.g. dry adiabats)
258
259    Attributes
260    ----------
261    ax : `matplotlib.axes.Axes`
262        The underlying Axes instance, which can be used for calling additional
263        plot functions (e.g. `axvline`)
264
265    """
266
267    def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5):
268        r"""Create SkewT - logP plots.
269
270        Parameters
271        ----------
272        fig : matplotlib.figure.Figure, optional
273            Source figure to use for plotting. If none is given, a new
274            :class:`matplotlib.figure.Figure` instance will be created.
275        rotation : float or int, optional
276            Controls the rotation of temperature relative to horizontal. Given
277            in degrees counterclockwise from x-axis. Defaults to 30 degrees.
278        subplot : tuple[int, int, int] or `matplotlib.gridspec.SubplotSpec` instance, optional
279            Controls the size/position of the created subplot. This allows creating
280            the skewT as part of a collection of subplots. If subplot is a tuple, it
281            should conform to the specification used for
282            :meth:`matplotlib.figure.Figure.add_subplot`. The
283            :class:`matplotlib.gridspec.SubplotSpec`
284            can be created by using :class:`matplotlib.gridspec.GridSpec`.
285        rect : tuple[float, float, float, float], optional
286            Rectangle (left, bottom, width, height) in which to place the axes. This
287            allows the user to place the axes at an arbitrary point on the figure.
288        aspect : float, int, or 'auto', optional
289            Aspect ratio (i.e. ratio of y-scale to x-scale) to maintain in the plot.
290            Defaults to 80.5. Passing the string ``'auto'`` tells matplotlib to handle
291            the aspect ratio automatically (this is not recommended for SkewT).
292
293        """
294        if fig is None:
295            import matplotlib.pyplot as plt
296            figsize = plt.rcParams.get('figure.figsize', (7, 7))
297            fig = plt.figure(figsize=figsize)
298        self._fig = fig
299
300        if rect and subplot:
301            raise ValueError("Specify only one of `rect' and `subplot', but not both")
302
303        elif rect:
304            self.ax = fig.add_axes(rect, projection='skewx', rotation=rotation)
305
306        else:
307            if subplot is not None:
308                # Handle being passed a tuple for the subplot, or a GridSpec instance
309                try:
310                    len(subplot)
311                except TypeError:
312                    subplot = (subplot,)
313            else:
314                subplot = (1, 1, 1)
315
316            self.ax = fig.add_subplot(*subplot, projection='skewx', rotation=rotation)
317
318        # Set the yaxis as inverted with log scaling
319        self.ax.set_yscale('log')
320
321        # Override default ticking for log scaling
322        self.ax.yaxis.set_major_formatter(ScalarFormatter())
323        self.ax.yaxis.set_major_locator(MultipleLocator(100))
324        self.ax.yaxis.set_minor_formatter(NullFormatter())
325
326        # Needed to make sure matplotlib doesn't freak out and create a bunch of ticks
327        # Also takes care of inverting the y-axis
328        self.ax.set_ylim(1050, 100)
329        self.ax.yaxis.set_units(units.hPa)
330
331        # Try to make sane default temperature plotting ticks
332        self.ax.xaxis.set_major_locator(MultipleLocator(10))
333        self.ax.xaxis.set_units(units.degC)
334        self.ax.set_xlim(-40, 50)
335        self.ax.grid(True)
336
337        self.mixing_lines = None
338        self.dry_adiabats = None
339        self.moist_adiabats = None
340
341        # Maintain a reasonable ratio of data limits. Only works on Matplotlib >= 3.2
342        if matplotlib.__version__[:3] > '3.1':
343            self.ax.set_aspect(aspect, adjustable='box')
344
345    def plot(self, pressure, t, *args, **kwargs):
346        r"""Plot data.
347
348        Simple wrapper around plot so that pressure is the first (independent)
349        input. This is essentially a wrapper around `plot`.
350
351        Parameters
352        ----------
353        pressure : array_like
354            pressure values
355        t : array_like
356            temperature values, can also be used for things like dew point
357        args
358            Other positional arguments to pass to :func:`~matplotlib.pyplot.plot`
359        kwargs
360            Other keyword arguments to pass to :func:`~matplotlib.pyplot.plot`
361
362        Returns
363        -------
364        list[matplotlib.lines.Line2D]
365            lines plotted
366
367        See Also
368        --------
369        :func:`matplotlib.pyplot.plot`
370
371        """
372        # Skew-T logP plotting
373        t, pressure = _delete_masked_points(t, pressure)
374        return self.ax.plot(t, pressure, *args, **kwargs)
375
376    def plot_barbs(self, pressure, u, v, c=None, xloc=1.0, x_clip_radius=0.1,
377                   y_clip_radius=0.08, **kwargs):
378        r"""Plot wind barbs.
379
380        Adds wind barbs to the skew-T plot. This is a wrapper around the
381        `barbs` command that adds to appropriate transform to place the
382        barbs in a vertical line, located as a function of pressure.
383
384        Parameters
385        ----------
386        pressure : array_like
387            pressure values
388        u : array_like
389            U (East-West) component of wind
390        v : array_like
391            V (North-South) component of wind
392        c:
393            An optional array used to map colors to the barbs
394        xloc : float, optional
395            Position for the barbs, in normalized axes coordinates, where 0.0
396            denotes far left and 1.0 denotes far right. Defaults to far right.
397        x_clip_radius : float, optional
398            Space, in normalized axes coordinates, to leave before clipping
399            wind barbs in the x-direction. Defaults to 0.1.
400        y_clip_radius : float, optional
401            Space, in normalized axes coordinates, to leave above/below plot
402            before clipping wind barbs in the y-direction. Defaults to 0.08.
403        plot_units: `pint.unit`
404            Units to plot in (performing conversion if necessary). Defaults to given units.
405        kwargs
406            Other keyword arguments to pass to :func:`~matplotlib.pyplot.barbs`
407
408        Returns
409        -------
410        matplotlib.quiver.Barbs
411            instance created
412
413        See Also
414        --------
415        :func:`matplotlib.pyplot.barbs`
416
417        """
418        # If plot_units specified, convert the data to those units
419        plotting_units = kwargs.pop('plot_units', None)
420        if plotting_units:
421            if hasattr(u, 'units') and hasattr(v, 'units'):
422                u = u.to(plotting_units)
423                v = v.to(plotting_units)
424            else:
425                raise ValueError('To convert to plotting units, units must be attached to '
426                                 'u and v wind components.')
427
428        # Assemble array of x-locations in axes space
429        x = np.empty_like(pressure)
430        x.fill(xloc)
431
432        # Do barbs plot at this location
433        if c is not None:
434            b = self.ax.barbs(x, pressure, u, v, c,
435                              transform=self.ax.get_yaxis_transform(which='tick2'),
436                              clip_on=True, zorder=2, **kwargs)
437        else:
438            b = self.ax.barbs(x, pressure, u, v,
439                              transform=self.ax.get_yaxis_transform(which='tick2'),
440                              clip_on=True, zorder=2, **kwargs)
441
442        # Override the default clip box, which is the axes rectangle, so we can have
443        # barbs that extend outside.
444        ax_bbox = transforms.Bbox([[xloc - x_clip_radius, -y_clip_radius],
445                                   [xloc + x_clip_radius, 1.0 + y_clip_radius]])
446        b.set_clip_box(transforms.TransformedBbox(ax_bbox, self.ax.transAxes))
447        return b
448
449    def plot_dry_adiabats(self, t0=None, pressure=None, **kwargs):
450        r"""Plot dry adiabats.
451
452        Adds dry adiabats (lines of constant potential temperature) to the
453        plot. The default style of these lines is dashed red lines with an alpha
454        value of 0.5. These can be overridden using keyword arguments.
455
456        Parameters
457        ----------
458        t0 : array_like, optional
459            Starting temperature values in Kelvin. If none are given, they will be
460            generated using the current temperature range at the bottom of
461            the plot.
462        pressure : array_like, optional
463            Pressure values to be included in the dry adiabats. If not
464            specified, they will be linearly distributed across the current
465            plotted pressure range.
466        kwargs
467            Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection`
468
469        Returns
470        -------
471        matplotlib.collections.LineCollection
472            instance created
473
474        See Also
475        --------
476        :func:`~metpy.calc.thermo.dry_lapse`
477        :meth:`plot_moist_adiabats`
478        :class:`matplotlib.collections.LineCollection`
479
480        """
481        # Remove old lines
482        if self.dry_adiabats:
483            self.dry_adiabats.remove()
484
485        # Determine set of starting temps if necessary
486        if t0 is None:
487            xmin, xmax = self.ax.get_xlim()
488            t0 = units.Quantity(np.arange(xmin, xmax + 1, 10), 'degC')
489
490        # Get pressure levels based on ylims if necessary
491        if pressure is None:
492            pressure = units.Quantity(np.linspace(*self.ax.get_ylim()), 'mbar')
493
494        # Assemble into data for plotting
495        t = dry_lapse(pressure, t0[:, np.newaxis],
496                      units.Quantity(1000., 'mbar')).to(units.degC)
497        linedata = [np.vstack((ti.m, pressure.m)).T for ti in t]
498
499        # Add to plot
500        kwargs.setdefault('colors', 'r')
501        kwargs.setdefault('linestyles', 'dashed')
502        kwargs.setdefault('alpha', 0.5)
503        self.dry_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs))
504        return self.dry_adiabats
505
506    def plot_moist_adiabats(self, t0=None, pressure=None, **kwargs):
507        r"""Plot moist adiabats.
508
509        Adds saturated pseudo-adiabats (lines of constant equivalent potential
510        temperature) to the plot. The default style of these lines is dashed
511        blue lines with an alpha value of 0.5. These can be overridden using
512        keyword arguments.
513
514        Parameters
515        ----------
516        t0 : array_like, optional
517            Starting temperature values in Kelvin. If none are given, they will be
518            generated using the current temperature range at the bottom of
519            the plot.
520        pressure : array_like, optional
521            Pressure values to be included in the moist adiabats. If not
522            specified, they will be linearly distributed across the current
523            plotted pressure range.
524        kwargs
525            Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection`
526
527        Returns
528        -------
529        matplotlib.collections.LineCollection
530            instance created
531
532        See Also
533        --------
534        :func:`~metpy.calc.thermo.moist_lapse`
535        :meth:`plot_dry_adiabats`
536        :class:`matplotlib.collections.LineCollection`
537
538        """
539        # Remove old lines
540        if self.moist_adiabats:
541            self.moist_adiabats.remove()
542
543        # Determine set of starting temps if necessary
544        if t0 is None:
545            xmin, xmax = self.ax.get_xlim()
546            t0 = units.Quantity(np.concatenate((np.arange(xmin, 0, 10),
547                                                np.arange(0, xmax + 1, 5))), 'degC')
548
549        # Get pressure levels based on ylims if necessary
550        if pressure is None:
551            pressure = units.Quantity(np.linspace(*self.ax.get_ylim()), 'mbar')
552
553        # Assemble into data for plotting
554        t = moist_lapse(pressure, t0[:, np.newaxis],
555                        units.Quantity(1000., 'mbar')).to(units.degC)
556        linedata = [np.vstack((ti.m, pressure.m)).T for ti in t]
557
558        # Add to plot
559        kwargs.setdefault('colors', 'b')
560        kwargs.setdefault('linestyles', 'dashed')
561        kwargs.setdefault('alpha', 0.5)
562        self.moist_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs))
563        return self.moist_adiabats
564
565    def plot_mixing_lines(self, mixing_ratio=None, pressure=None, **kwargs):
566        r"""Plot lines of constant mixing ratio.
567
568        Adds lines of constant mixing ratio (isohumes) to the
569        plot. The default style of these lines is dashed green lines with an
570        alpha value of 0.8. These can be overridden using keyword arguments.
571
572        Parameters
573        ----------
574        mixing_ratio : array_like, optional
575            Unitless mixing ratio values to plot. If none are given, default
576            values are used.
577        pressure : array_like, optional
578            Pressure values to be included in the isohumes. If not
579            specified, they will be linearly distributed across the current
580            plotted pressure range up to 600 mb.
581        kwargs
582            Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection`
583
584        Returns
585        -------
586        matplotlib.collections.LineCollection
587            instance created
588
589        See Also
590        --------
591        :class:`matplotlib.collections.LineCollection`
592
593        """
594        # Remove old lines
595        if self.mixing_lines:
596            self.mixing_lines.remove()
597
598        # Default mixing level values if necessary
599        if mixing_ratio is None:
600            mixing_ratio = np.array([0.0004, 0.001, 0.002, 0.004, 0.007, 0.01,
601                                     0.016, 0.024, 0.032]).reshape(-1, 1)
602
603        # Set pressure range if necessary
604        if pressure is None:
605            pressure = units.Quantity(np.linspace(600, max(self.ax.get_ylim())), 'mbar')
606
607        # Assemble data for plotting
608        td = dewpoint(vapor_pressure(pressure, mixing_ratio))
609        linedata = [np.vstack((t.m, pressure.m)).T for t in td]
610
611        # Add to plot
612        kwargs.setdefault('colors', 'g')
613        kwargs.setdefault('linestyles', 'dashed')
614        kwargs.setdefault('alpha', 0.8)
615        self.mixing_lines = self.ax.add_collection(LineCollection(linedata, **kwargs))
616        return self.mixing_lines
617
618    def shade_area(self, y, x1, x2=0, which='both', **kwargs):
619        r"""Shade area between two curves.
620
621        Shades areas between curves. Area can be where one is greater or less than the other
622        or all areas shaded.
623
624        Parameters
625        ----------
626        y : array_like
627            1-dimensional array of numeric y-values
628        x1 : array_like
629            1-dimensional array of numeric x-values
630        x2 : array_like
631            1-dimensional array of numeric x-values
632        which : string
633            Specifies if `positive`, `negative`, or `both` areas are being shaded.
634            Will be overridden by where.
635        kwargs
636            Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection`
637
638        Returns
639        -------
640        :class:`matplotlib.collections.PolyCollection`
641
642        See Also
643        --------
644        :class:`matplotlib.collections.PolyCollection`
645        :func:`matplotlib.axes.Axes.fill_betweenx`
646
647        """
648        fill_properties = {'positive':
649                           {'facecolor': 'tab:red', 'alpha': 0.4, 'where': x1 > x2},
650                           'negative':
651                           {'facecolor': 'tab:blue', 'alpha': 0.4, 'where': x1 < x2},
652                           'both':
653                           {'facecolor': 'tab:green', 'alpha': 0.4, 'where': None}}
654
655        try:
656            fill_args = fill_properties[which]
657            fill_args.update(kwargs)
658        except KeyError:
659            raise ValueError(f'Unknown option for which: {which}') from None
660
661        arrs = y, x1, x2
662
663        if fill_args['where'] is not None:
664            arrs = arrs + (fill_args['where'],)
665            fill_args.pop('where', None)
666
667        fill_args['interpolate'] = True
668
669        arrs = _delete_masked_points(*arrs)
670
671        return self.ax.fill_betweenx(*arrs, **fill_args)
672
673    def shade_cape(self, pressure, t, t_parcel, **kwargs):
674        r"""Shade areas of Convective Available Potential Energy (CAPE).
675
676        Shades areas where the parcel is warmer than the environment (areas of positive
677        buoyancy.
678
679        Parameters
680        ----------
681        pressure : array_like
682            Pressure values
683        t : array_like
684            Temperature values
685        dewpoint : array_like
686            Dewpoint values
687        t_parcel : array_like
688            Parcel path temperature values
689        limit_shading : bool
690            Eliminate shading below the LCL or above the EL, default is True
691        kwargs
692            Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection`
693
694        Returns
695        -------
696        :class:`matplotlib.collections.PolyCollection`
697
698        See Also
699        --------
700        :class:`matplotlib.collections.PolyCollection`
701        :func:`matplotlib.axes.Axes.fill_betweenx`
702
703        """
704        return self.shade_area(pressure, t_parcel, t, which='positive', **kwargs)
705
706    def shade_cin(self, pressure, t, t_parcel, dewpoint=None, **kwargs):
707        r"""Shade areas of Convective INhibition (CIN).
708
709        Shades areas where the parcel is cooler than the environment (areas of negative
710        buoyancy). If `dewpoint` is passed in, negative area below the lifting condensation
711        level or above the equilibrium level is not shaded.
712
713        Parameters
714        ----------
715        pressure : array_like
716            Pressure values
717        t : array_like
718            Temperature values
719        t_parcel : array_like
720            Parcel path temperature values
721        dewpoint : array_like
722            Dew point values, optional
723        kwargs
724            Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection`
725
726        Returns
727        -------
728        :class:`matplotlib.collections.PolyCollection`
729
730        See Also
731        --------
732        :class:`matplotlib.collections.PolyCollection`
733        :func:`matplotlib.axes.Axes.fill_betweenx`
734
735        """
736        if dewpoint is not None:
737            lcl_p, _ = lcl(pressure[0], t[0], dewpoint[0])
738            el_p, _ = el(pressure, t, dewpoint, t_parcel)
739            idx = np.logical_and(pressure > el_p, pressure < lcl_p)
740        else:
741            idx = np.arange(0, len(pressure))
742        return self.shade_area(pressure[idx], t_parcel[idx], t[idx], which='negative',
743                               **kwargs)
744
745
746@exporter.export
747class Hodograph:
748    r"""Make a hodograph of wind data.
749
750    Plots the u and v components of the wind along the x and y axes, respectively.
751
752    This class simplifies the process of creating a hodograph using matplotlib.
753    It provides helpers for creating a circular grid and for plotting the wind as a line
754    colored by another value (such as wind speed).
755
756    Attributes
757    ----------
758    ax : `matplotlib.axes.Axes`
759        The underlying Axes instance used for all plotting
760
761    """
762
763    def __init__(self, ax=None, component_range=80):
764        r"""Create a Hodograph instance.
765
766        Parameters
767        ----------
768        ax : `matplotlib.axes.Axes`, optional
769            The `Axes` instance used for plotting
770        component_range : value
771            The maximum range of the plot. Used to set plot bounds and control the maximum
772            number of grid rings needed.
773
774        """
775        if ax is None:
776            import matplotlib.pyplot as plt
777            self.ax = plt.figure().add_subplot(1, 1, 1)
778        else:
779            self.ax = ax
780        self.ax.set_aspect('equal', 'box')
781        self.ax.set_xlim(-component_range, component_range)
782        self.ax.set_ylim(-component_range, component_range)
783
784        # == sqrt(2) * max_range, which is the distance at the corner
785        self.max_range = 1.4142135 * component_range
786
787    def add_grid(self, increment=10., **kwargs):
788        r"""Add grid lines to hodograph.
789
790        Creates lines for the x- and y-axes, as well as circles denoting wind speed values.
791
792        Parameters
793        ----------
794        increment : value, optional
795            The value increment between rings
796        kwargs
797            Other kwargs to control appearance of lines
798
799        See Also
800        --------
801        :class:`matplotlib.patches.Circle`
802        :meth:`matplotlib.axes.Axes.axhline`
803        :meth:`matplotlib.axes.Axes.axvline`
804
805        """
806        # Some default arguments. Take those, and update with any
807        # arguments passed in
808        grid_args = {'color': 'grey', 'linestyle': 'dashed'}
809        if kwargs:
810            grid_args.update(kwargs)
811
812        # Take those args and make appropriate for a Circle
813        circle_args = grid_args.copy()
814        color = circle_args.pop('color', None)
815        circle_args['edgecolor'] = color
816        circle_args['fill'] = False
817
818        self.rings = []
819        for r in np.arange(increment, self.max_range, increment):
820            c = Circle((0, 0), radius=r, **circle_args)
821            self.ax.add_patch(c)
822            self.rings.append(c)
823
824        # Add lines for x=0 and y=0
825        self.yaxis = self.ax.axvline(0, **grid_args)
826        self.xaxis = self.ax.axhline(0, **grid_args)
827
828    @staticmethod
829    def _form_line_args(kwargs):
830        """Simplify taking the default line style and extending with kwargs."""
831        def_args = {'linewidth': 3}
832        def_args.update(kwargs)
833        return def_args
834
835    def plot(self, u, v, **kwargs):
836        r"""Plot u, v data.
837
838        Plots the wind data on the hodograph.
839
840        Parameters
841        ----------
842        u : array_like
843            u-component of wind
844        v : array_like
845            v-component of wind
846        kwargs
847            Other keyword arguments to pass to :meth:`matplotlib.axes.Axes.plot`
848
849        Returns
850        -------
851        list[matplotlib.lines.Line2D]
852            lines plotted
853
854        See Also
855        --------
856        :meth:`Hodograph.plot_colormapped`
857
858        """
859        line_args = self._form_line_args(kwargs)
860        u, v = _delete_masked_points(u, v)
861        return self.ax.plot(u, v, **line_args)
862
863    def wind_vectors(self, u, v, **kwargs):
864        r"""Plot u, v data as wind vectors.
865
866        Plot the wind data as vectors for each level, beginning at the origin.
867
868        Parameters
869        ----------
870        u : array_like
871            u-component of wind
872        v : array_like
873            v-component of wind
874        kwargs
875            Other keyword arguments to pass to :meth:`matplotlib.axes.Axes.quiver`
876
877        Returns
878        -------
879        matplotlib.quiver.Quiver
880            arrows plotted
881
882        """
883        quiver_args = {'units': 'xy', 'scale': 1}
884        quiver_args.update(**kwargs)
885        center_position = np.zeros_like(u)
886        return self.ax.quiver(center_position, center_position,
887                              u, v, **quiver_args)
888
889    def plot_colormapped(self, u, v, c, intervals=None, colors=None, **kwargs):
890        r"""Plot u, v data, with line colored based on a third set of data.
891
892        Plots the wind data on the hodograph, but with a colormapped line. Takes a third
893        variable besides the winds (e.g. heights or pressure levels) and either a colormap to
894        color it with or a series of contour intervals and colors to create a colormap and
895        norm to control colormapping. The intervals must always be in increasing
896        order. For using custom contour intervals with height data, the function will
897        automatically interpolate to the contour intervals from the height and wind data,
898        as well as convert the input contour intervals from height AGL to MSL to work with the
899        provided heights.
900
901        Parameters
902        ----------
903        u : array_like
904            u-component of wind
905        v : array_like
906            v-component of wind
907        c : array_like
908            data to use for colormapping (e.g. heights, pressure, wind speed)
909        intervals: array-like, optional
910            Array of intervals for c to use in coloring the hodograph.
911        colors: list, optional
912            Array of strings representing colors for the hodograph segments.
913        kwargs
914            Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection`
915
916        Returns
917        -------
918        matplotlib.collections.LineCollection
919            instance created
920
921        See Also
922        --------
923        :meth:`Hodograph.plot`
924
925        """
926        u, v, c = _delete_masked_points(u, v, c)
927
928        # Plotting a color segmented hodograph
929        if colors:
930            cmap = mcolors.ListedColormap(colors)
931            # If we are segmenting by height (a length), interpolate the contour intervals
932            if intervals.check('[length]'):
933
934                # Find any intervals not in the data and interpolate them
935                heights_min = np.nanmin(c)
936                heights_max = np.nanmax(c)
937                interpolation_heights = np.array([bound.m for bound in intervals
938                                                  if bound not in c
939                                                  and heights_min <= bound <= heights_max])
940                interpolation_heights = units.Quantity(np.sort(interpolation_heights),
941                                                       intervals.units)
942                interpolated_u, interpolated_v = interpolate_1d(interpolation_heights, c, u, v)
943
944                # Combine the interpolated data with the actual data
945                c = concatenate([c, interpolation_heights])
946                u = concatenate([u, interpolated_u])
947                v = concatenate([v, interpolated_v])
948                sort_inds = np.argsort(c)
949                c = c[sort_inds]
950                u = u[sort_inds]
951                v = v[sort_inds]
952
953                # Unit conversion required for coloring of bounds/data in dissimilar units
954                # to work properly.
955                c = c.to_base_units()  # TODO: This shouldn't be required!
956                intervals = intervals.to_base_units()
957
958            norm = mcolors.BoundaryNorm(intervals.magnitude, cmap.N)
959            cmap.set_over('none')
960            cmap.set_under('none')
961            kwargs['cmap'] = cmap
962            kwargs['norm'] = norm
963            line_args = self._form_line_args(kwargs)
964
965        # Plotting a continuously colored line
966        else:
967            line_args = self._form_line_args(kwargs)
968
969        # Do the plotting
970        lc = colored_line(u, v, c, **line_args)
971        self.ax.add_collection(lc)
972        return lc
973