Completed
Pull Request — master (#382)
by
unknown
01:18
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 between two curves.
528
529
        Shades areas between curves. Area can be where one is greater or less than the other
530
        or all areas shaded.
531
532
        Parameters
533
        ----------
534
        y : array_like
535
            1-dimensional array of numeric y-values
536
        x1 : array_like
537
            1-dimensional array of numeric x-values
538
        x2 : array_like
539
            1-dimensional array of numeric x-values
540
        which : string
541
            Specifies if `positive`, `negative`, or `both` areas are being shaded.
542
            Will be overridden by where.
543
        kwargs
544
            Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection`
545
546
        Returns
547
        -------
548
        :class:`matplotlib.collections.PolyCollection`
549
550
        See Also
551
        --------
552
        :class:`matplotlib.collections.PolyCollection`
553
        :method:`matplotlib.axes.Axes.fill_betweenx`
554
        """
555
        fill_properties = {'positive':
556
                               {'facecolor': 'tab:red', 'alpha': 0.4, 'where': x1 > x2},
557
                           'negative':
558
                               {'facecolor': 'tab:blue', 'alpha': 0.4, 'where': x1 < x2},
559
                           'both':
560
                               {'facecolor': 'tab:green', 'alpha' :0.4, 'where': None}}
561
562
        try:
563
            fill_args = fill_properties[which]
564
            fill_args.update(kwargs)
565
        except KeyError:
566
            raise ValueError('Unknown option for which: {0}'.format(str(which)))
567
568
        arrs = y, x1, x2
569
570
        if fill_args['where'] is not None:
571
            arrs = arrs + (fill_args['where'],)
572
            fill_args.pop('where', None)
573
574
        arrs = delete_masked_points(*arrs)
575
576
        return self.ax.fill_betweenx(*arrs, **fill_args)
577
578
    def shade_cape(self, p, T, T_parcel, **kwargs):
579
        r"""Shades areas of CAPE.
580
581
        Shades areas where the parcel is warmer than the environment (areas of positive
582
        buoyancy.
583
584
        Parameters
585
        ----------
586
        p : array_like
587
            Pressure values
588
        T : array_like
589
            Temperature values
590
        T_parcel : array_like
591
            Parcel path temperature values
592
        kwargs
593
            Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection`
594
595
        Returns
596
        -------
597
        :class:`matplotlib.collections.PolyCollection`
598
599
        See Also
600
        --------
601
        :class:`matplotlib.collections.PolyCollection`
602
        :method:`matplotlib.axes.Axes.fill_betweenx`
603
        """
604
        return self.shade_area(p, T_parcel, T, which='positive', **kwargs)
605
606
    def shade_cin(self, p, T, T_parcel, **kwargs):
607
        r"""Shades areas of CIN.
608
609
        Shades areas where the parcel is cooler than the environment (areas of negative
610
        buoyancy.
611
612
        Parameters
613
        ----------
614
        p : array_like
615
            Pressure values
616
        T : array_like
617
            Temperature values
618
        T_parcel : array_like
619
            Parcel path temperature values
620
        kwargs
621
            Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection`
622
623
        Returns
624
        -------
625
        :class:`matplotlib.collections.PolyCollection`
626
627
        See Also
628
        --------
629
        :class:`matplotlib.collections.PolyCollection`
630
        :method:`matplotlib.axes.Axes.fill_betweenx`
631
        """
632
        return self.shade_area(p, T_parcel, T, which='negative', **kwargs)
633
634
@exporter.export
635
class Hodograph(object):
636
    r"""Make a hodograph of wind data.
637
638
    Plots the u and v components of the wind along the x and y axes, respectively.
639
640
    This class simplifies the process of creating a hodograph using matplotlib.
641
    It provides helpers for creating a circular grid and for plotting the wind as a line
642
    colored by another value (such as wind speed).
643
644
    Attributes
645
    ----------
646
    ax : `matplotlib.axes.Axes`
647
        The underlying Axes instance used for all plotting
648
    """
649
650
    def __init__(self, ax=None, component_range=80):
651
        r"""Create a Hodograph instance.
652
653
        Parameters
654
        ----------
655
        ax : `matplotlib.axes.Axes`, optional
656
            The `Axes` instance used for plotting
657
        component_range : value
658
            The maximum range of the plot. Used to set plot bounds and control the maximum
659
            number of grid rings needed.
660
        """
661
        if ax is None:
662
            import matplotlib.pyplot as plt
663
            self.ax = plt.figure().add_subplot(1, 1, 1)
664
        else:
665
            self.ax = ax
666
        self.ax.set_aspect('equal', 'box')
667
        self.ax.set_xlim(-component_range, component_range)
668
        self.ax.set_ylim(-component_range, component_range)
669
670
        # == sqrt(2) * max_range, which is the distance at the corner
671
        self.max_range = 1.4142135 * component_range
672
673
    def add_grid(self, increment=10., **kwargs):
674
        r"""Add grid lines to hodograph.
675
676
        Creates lines for the x- and y-axes, as well as circles denoting wind speed values.
677
678
        Parameters
679
        ----------
680
        increment : value, optional
681
            The value increment between rings
682
        kwargs
683
            Other kwargs to control appearance of lines
684
685
        See Also
686
        --------
687
        :class:`matplotlib.patches.Circle`
688
        :meth:`matplotlib.axes.Axes.axhline`
689
        :meth:`matplotlib.axes.Axes.axvline`
690
        """
691
        # Some default arguments. Take those, and update with any
692
        # arguments passed in
693
        grid_args = dict(color='grey', linestyle='dashed')
694
        if kwargs:
695
            grid_args.update(kwargs)
696
697
        # Take those args and make appropriate for a Circle
698
        circle_args = grid_args.copy()
699
        color = circle_args.pop('color', None)
700
        circle_args['edgecolor'] = color
701
        circle_args['fill'] = False
702
703
        self.rings = []
704
        for r in np.arange(increment, self.max_range, increment):
705
            c = Circle((0, 0), radius=r, **circle_args)
706
            self.ax.add_patch(c)
707
            self.rings.append(c)
708
709
        # Add lines for x=0 and y=0
710
        self.yaxis = self.ax.axvline(0, **grid_args)
711
        self.xaxis = self.ax.axhline(0, **grid_args)
712
713
    @staticmethod
714
    def _form_line_args(kwargs):
715
        """Simplify taking the default line style and extending with kwargs."""
716
        def_args = dict(linewidth=3)
717
        def_args.update(kwargs)
718
        return def_args
719
720
    def plot(self, u, v, **kwargs):
721
        r"""Plot u, v data.
722
723
        Plots the wind data on the hodograph.
724
725
        Parameters
726
        ----------
727
        u : array_like
728
            u-component of wind
729
        v : array_like
730
            v-component of wind
731
        kwargs
732
            Other keyword arguments to pass to :meth:`matplotlib.axes.Axes.plot`
733
734
        Returns
735
        -------
736
        list[matplotlib.lines.Line2D]
737
            lines plotted
738
739
        See Also
740
        --------
741
        :meth:`Hodograph.plot_colormapped`
742
        """
743
        line_args = self._form_line_args(kwargs)
744
        u, v = delete_masked_points(u, v)
745
        return self.ax.plot(u, v, **line_args)
746
747
    def plot_colormapped(self, u, v, c, **kwargs):
748
        r"""Plot u, v data, with line colored based on a third set of data.
749
750
        Plots the wind data on the hodograph, but with a colormapped line.
751
752
        Simple wrapper around plot so that pressure is the first (independent)
753
        input. This is essentially a wrapper around `semilogy`. It also
754
        sets some appropriate ticking and plot ranges.
755
756
        Parameters
757
        ----------
758
        u : array_like
759
            u-component of wind
760
        v : array_like
761
            v-component of wind
762
        c : array_like
763
            data to use for colormapping
764
        kwargs
765
            Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection`
766
767
        Returns
768
        -------
769
        matplotlib.collections.LineCollection
770
            instance created
771
772
        See Also
773
        --------
774
        :meth:`Hodograph.plot`
775
        """
776
        line_args = self._form_line_args(kwargs)
777
        u, v, c = delete_masked_points(u, v, c)
778
        lc = colored_line(u, v, c, **line_args)
779
        self.ax.add_collection(lc)
780
        return lc
781