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