Completed
Pull Request — master (#382)
by Ryan
01:22
created

SkewT.shade_cape()   B

Complexity

Conditions 1

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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