Completed
Push — master ( 63a522...205a54 )
by Ryan
01:09
created

SkewXTick.update_position()   A

Complexity

Conditions 1

Size

Total Lines 6

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