Completed
Pull Request — master (#258)
by Ryan
01:13
created

SkewXTick._need_upper()   A

Complexity

Conditions 1

Size

Total Lines 2

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