1# Copyright (c) 2014,2015,2016,2017,2019 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 6Contain 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 10from contextlib import ExitStack 11import warnings 12 13import matplotlib 14from matplotlib.axes import Axes 15import matplotlib.axis as maxis 16from matplotlib.collections import LineCollection 17import matplotlib.colors as mcolors 18from matplotlib.patches import Circle 19from matplotlib.projections import register_projection 20import matplotlib.spines as mspines 21from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter 22import matplotlib.transforms as transforms 23import numpy as np 24 25from ._util import colored_line 26from ..calc import dewpoint, dry_lapse, el, lcl, moist_lapse, vapor_pressure 27from ..calc.tools import _delete_masked_points 28from ..interpolate import interpolate_1d 29from ..package_tools import Exporter 30from ..units import concatenate, units 31 32exporter = Exporter(globals()) 33 34 35class SkewTTransform(transforms.Affine2D): 36 """Perform Skew transform for Skew-T plotting. 37 38 This works in pixel space, so is designed to be applied after the normal plotting 39 transformations. 40 """ 41 42 def __init__(self, bbox, rot): 43 """Initialize skew transform. 44 45 This needs a reference to the parent bounding box to do the appropriate math and 46 to register it as a child so that the transform is invalidated and regenerated if 47 the bounding box changes. 48 """ 49 super().__init__() 50 self._bbox = bbox 51 self.set_children(bbox) 52 self.invalidate() 53 54 # We're not trying to support changing the rotation, so go ahead and convert to 55 # the right factor for skewing here and just save that. 56 self._rot_factor = np.tan(np.deg2rad(rot)) 57 58 def get_matrix(self): 59 """Return transformation matrix.""" 60 if self._invalid: 61 # The following matrix is equivalent to the following: 62 # x0, y0 = self._bbox.xmin, self._bbox.ymin 63 # self.translate(-x0, -y0).skew_deg(self._rot, 0).translate(x0, y0) 64 # Setting it this way is just more efficient. 65 self._mtx = np.array([[1.0, self._rot_factor, -self._rot_factor * self._bbox.ymin], 66 [0.0, 1.0, 0.0], 67 [0.0, 0.0, 1.0]]) 68 69 # Need to clear both the invalid flag *and* reset the inverse, which is cached 70 # by the parent class. 71 self._invalid = 0 72 self._inverted = None 73 return self._mtx 74 75 76class SkewXTick(maxis.XTick): 77 r"""Make x-axis ticks for Skew-T plots. 78 79 This class adds to the standard :class:`matplotlib.axis.XTick` dynamic checking 80 for whether a top or bottom tick is actually within the data limits at that part 81 and draw as appropriate. It also performs similar checking for gridlines. 82 """ 83 84 # Taken from matplotlib's SkewT example to update for matplotlib 3.1's changes to 85 # state management for ticks. See matplotlib/matplotlib#10088 86 def draw(self, renderer): 87 """Draw the tick.""" 88 # When adding the callbacks with `stack.callback`, we fetch the current 89 # visibility state of the artist with `get_visible`; the ExitStack will 90 # restore these states (`set_visible`) at the end of the block (after 91 # the draw). 92 with ExitStack() as stack: 93 for artist in [self.gridline, self.tick1line, self.tick2line, 94 self.label1, self.label2]: 95 stack.callback(artist.set_visible, artist.get_visible()) 96 97 self.tick1line.set_visible(self.tick1line.get_visible() and self.lower_in_bounds) 98 self.label1.set_visible(self.label1.get_visible() and self.lower_in_bounds) 99 self.tick2line.set_visible(self.tick2line.get_visible() and self.upper_in_bounds) 100 self.label2.set_visible(self.label2.get_visible() and self.upper_in_bounds) 101 self.gridline.set_visible(self.gridline.get_visible() and self.grid_in_bounds) 102 super().draw(renderer) 103 104 @property 105 def lower_in_bounds(self): 106 """Whether the lower part of the tick is in bounds.""" 107 return transforms.interval_contains(self.axes.lower_xlim, self.get_loc()) 108 109 @property 110 def upper_in_bounds(self): 111 """Whether the upper part of the tick is in bounds.""" 112 return transforms.interval_contains(self.axes.upper_xlim, self.get_loc()) 113 114 @property 115 def grid_in_bounds(self): 116 """Whether any of the tick grid line is in bounds.""" 117 return transforms.interval_contains(self.axes.xaxis.get_view_interval(), 118 self.get_loc()) 119 120 121class SkewXAxis(maxis.XAxis): 122 r"""Make an x-axis that works properly for Skew-T plots. 123 124 This class exists to force the use of our custom :class:`SkewXTick` as well 125 as provide a custom value for interval that combines the extents of the 126 upper and lower x-limits from the axes. 127 """ 128 129 def _get_tick(self, major): 130 # Warning stuff can go away when we only support Matplotlib >=3.3 131 with warnings.catch_warnings(): 132 warnings.simplefilter('ignore', getattr( 133 matplotlib, 'MatplotlibDeprecationWarning', DeprecationWarning)) 134 return SkewXTick(self.axes, None, label=None, major=major) 135 136 # Needed to properly handle tight bbox 137 def _get_tick_bboxes(self, ticks, renderer): 138 """Return lists of bboxes for ticks' label1's and label2's.""" 139 return ([tick.label1.get_window_extent(renderer) 140 for tick in ticks if tick.label1.get_visible() and tick.lower_in_bounds], 141 [tick.label2.get_window_extent(renderer) 142 for tick in ticks if tick.label2.get_visible() and tick.upper_in_bounds]) 143 144 def get_view_interval(self): 145 """Get the view interval.""" 146 return self.axes.upper_xlim[0], self.axes.lower_xlim[1] 147 148 149class SkewSpine(mspines.Spine): 150 r"""Make an x-axis spine that works properly for Skew-T plots. 151 152 This class exists to use the separate x-limits from the axes to properly 153 locate the spine. 154 """ 155 156 def _adjust_location(self): 157 pts = self._path.vertices 158 if self.spine_type == 'top': 159 pts[:, 0] = self.axes.upper_xlim 160 else: 161 pts[:, 0] = self.axes.lower_xlim 162 163 164class SkewXAxes(Axes): 165 r"""Make a set of axes for Skew-T plots. 166 167 This class handles registration of the skew-xaxes as a projection as well as setting up 168 the appropriate transformations. It also makes sure we use our instances for spines 169 and x-axis: :class:`SkewSpine` and :class:`SkewXAxis`. It provides properties to 170 facilitate finding the x-limits for the bottom and top of the plot as well. 171 """ 172 173 # The projection must specify a name. This will be used be the 174 # user to select the projection, i.e. ``subplot(111, 175 # projection='skewx')``. 176 name = 'skewx' 177 178 def __init__(self, *args, **kwargs): 179 r"""Initialize `SkewXAxes`. 180 181 Parameters 182 ---------- 183 args : Arbitrary positional arguments 184 Passed to :class:`matplotlib.axes.Axes` 185 186 position: int, optional 187 The rotation of the x-axis against the y-axis, in degrees. 188 189 kwargs : Arbitrary keyword arguments 190 Passed to :class:`matplotlib.axes.Axes` 191 192 """ 193 # This needs to be popped and set before moving on 194 self.rot = kwargs.pop('rotation', 30) 195 super().__init__(*args, **kwargs) 196 197 def _init_axis(self): 198 # Taken from Axes and modified to use our modified X-axis 199 self.xaxis = SkewXAxis(self) 200 self.spines['top'].register_axis(self.xaxis) 201 self.spines['bottom'].register_axis(self.xaxis) 202 self.yaxis = maxis.YAxis(self) 203 self.spines['left'].register_axis(self.yaxis) 204 self.spines['right'].register_axis(self.yaxis) 205 206 def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): 207 # pylint: disable=unused-argument 208 spines = {'top': SkewSpine.linear_spine(self, 'top'), 209 'bottom': mspines.Spine.linear_spine(self, 'bottom'), 210 'left': mspines.Spine.linear_spine(self, 'left'), 211 'right': mspines.Spine.linear_spine(self, 'right')} 212 return spines 213 214 def _set_lim_and_transforms(self): 215 """Set limits and transforms. 216 217 This is called once when the plot is created to set up all the 218 transforms for the data, text and grids. 219 220 """ 221 # Get the standard transform setup from the Axes base class 222 super()._set_lim_and_transforms() 223 224 # This transformation handles the skewing 225 skew_trans = SkewTTransform(self.bbox, self.rot) 226 227 # Create the full transform from Data to Pixels 228 self.transData += skew_trans 229 230 # Blended transforms like this need to have the skewing applied using 231 # both axes, in axes coords like before. 232 self._xaxis_transform += skew_trans 233 234 @property 235 def lower_xlim(self): 236 """Get the data limits for the x-axis along the bottom of the axes.""" 237 return self.axes.viewLim.intervalx 238 239 @property 240 def upper_xlim(self): 241 """Get the data limits for the x-axis along the top of the axes.""" 242 return self.transData.inverted().transform([[self.bbox.xmin, self.bbox.ymax], 243 self.bbox.max])[:, 0] 244 245 246# Now register the projection with matplotlib so the user can select it. 247register_projection(SkewXAxes) 248 249 250@exporter.export 251class SkewT: 252 r"""Make Skew-T log-P plots of data. 253 254 This class simplifies the process of creating Skew-T log-P plots in 255 using matplotlib. It handles requesting the appropriate skewed projection, 256 and provides simplified wrappers to make it easy to plot data, add wind 257 barbs, and add other lines to the plots (e.g. dry adiabats) 258 259 Attributes 260 ---------- 261 ax : `matplotlib.axes.Axes` 262 The underlying Axes instance, which can be used for calling additional 263 plot functions (e.g. `axvline`) 264 265 """ 266 267 def __init__(self, fig=None, rotation=30, subplot=None, rect=None, aspect=80.5): 268 r"""Create SkewT - logP plots. 269 270 Parameters 271 ---------- 272 fig : matplotlib.figure.Figure, optional 273 Source figure to use for plotting. If none is given, a new 274 :class:`matplotlib.figure.Figure` instance will be created. 275 rotation : float or int, optional 276 Controls the rotation of temperature relative to horizontal. Given 277 in degrees counterclockwise from x-axis. Defaults to 30 degrees. 278 subplot : tuple[int, int, int] or `matplotlib.gridspec.SubplotSpec` instance, optional 279 Controls the size/position of the created subplot. This allows creating 280 the skewT as part of a collection of subplots. If subplot is a tuple, it 281 should conform to the specification used for 282 :meth:`matplotlib.figure.Figure.add_subplot`. The 283 :class:`matplotlib.gridspec.SubplotSpec` 284 can be created by using :class:`matplotlib.gridspec.GridSpec`. 285 rect : tuple[float, float, float, float], optional 286 Rectangle (left, bottom, width, height) in which to place the axes. This 287 allows the user to place the axes at an arbitrary point on the figure. 288 aspect : float, int, or 'auto', optional 289 Aspect ratio (i.e. ratio of y-scale to x-scale) to maintain in the plot. 290 Defaults to 80.5. Passing the string ``'auto'`` tells matplotlib to handle 291 the aspect ratio automatically (this is not recommended for SkewT). 292 293 """ 294 if fig is None: 295 import matplotlib.pyplot as plt 296 figsize = plt.rcParams.get('figure.figsize', (7, 7)) 297 fig = plt.figure(figsize=figsize) 298 self._fig = fig 299 300 if rect and subplot: 301 raise ValueError("Specify only one of `rect' and `subplot', but not both") 302 303 elif rect: 304 self.ax = fig.add_axes(rect, projection='skewx', rotation=rotation) 305 306 else: 307 if subplot is not None: 308 # Handle being passed a tuple for the subplot, or a GridSpec instance 309 try: 310 len(subplot) 311 except TypeError: 312 subplot = (subplot,) 313 else: 314 subplot = (1, 1, 1) 315 316 self.ax = fig.add_subplot(*subplot, projection='skewx', rotation=rotation) 317 318 # Set the yaxis as inverted with log scaling 319 self.ax.set_yscale('log') 320 321 # Override default ticking for log scaling 322 self.ax.yaxis.set_major_formatter(ScalarFormatter()) 323 self.ax.yaxis.set_major_locator(MultipleLocator(100)) 324 self.ax.yaxis.set_minor_formatter(NullFormatter()) 325 326 # Needed to make sure matplotlib doesn't freak out and create a bunch of ticks 327 # Also takes care of inverting the y-axis 328 self.ax.set_ylim(1050, 100) 329 self.ax.yaxis.set_units(units.hPa) 330 331 # Try to make sane default temperature plotting ticks 332 self.ax.xaxis.set_major_locator(MultipleLocator(10)) 333 self.ax.xaxis.set_units(units.degC) 334 self.ax.set_xlim(-40, 50) 335 self.ax.grid(True) 336 337 self.mixing_lines = None 338 self.dry_adiabats = None 339 self.moist_adiabats = None 340 341 # Maintain a reasonable ratio of data limits. Only works on Matplotlib >= 3.2 342 if matplotlib.__version__[:3] > '3.1': 343 self.ax.set_aspect(aspect, adjustable='box') 344 345 def plot(self, pressure, t, *args, **kwargs): 346 r"""Plot data. 347 348 Simple wrapper around plot so that pressure is the first (independent) 349 input. This is essentially a wrapper around `plot`. 350 351 Parameters 352 ---------- 353 pressure : array_like 354 pressure values 355 t : array_like 356 temperature values, can also be used for things like dew point 357 args 358 Other positional arguments to pass to :func:`~matplotlib.pyplot.plot` 359 kwargs 360 Other keyword arguments to pass to :func:`~matplotlib.pyplot.plot` 361 362 Returns 363 ------- 364 list[matplotlib.lines.Line2D] 365 lines plotted 366 367 See Also 368 -------- 369 :func:`matplotlib.pyplot.plot` 370 371 """ 372 # Skew-T logP plotting 373 t, pressure = _delete_masked_points(t, pressure) 374 return self.ax.plot(t, pressure, *args, **kwargs) 375 376 def plot_barbs(self, pressure, u, v, c=None, xloc=1.0, x_clip_radius=0.1, 377 y_clip_radius=0.08, **kwargs): 378 r"""Plot wind barbs. 379 380 Adds wind barbs to the skew-T plot. This is a wrapper around the 381 `barbs` command that adds to appropriate transform to place the 382 barbs in a vertical line, located as a function of pressure. 383 384 Parameters 385 ---------- 386 pressure : array_like 387 pressure values 388 u : array_like 389 U (East-West) component of wind 390 v : array_like 391 V (North-South) component of wind 392 c: 393 An optional array used to map colors to the barbs 394 xloc : float, optional 395 Position for the barbs, in normalized axes coordinates, where 0.0 396 denotes far left and 1.0 denotes far right. Defaults to far right. 397 x_clip_radius : float, optional 398 Space, in normalized axes coordinates, to leave before clipping 399 wind barbs in the x-direction. Defaults to 0.1. 400 y_clip_radius : float, optional 401 Space, in normalized axes coordinates, to leave above/below plot 402 before clipping wind barbs in the y-direction. Defaults to 0.08. 403 plot_units: `pint.unit` 404 Units to plot in (performing conversion if necessary). Defaults to given units. 405 kwargs 406 Other keyword arguments to pass to :func:`~matplotlib.pyplot.barbs` 407 408 Returns 409 ------- 410 matplotlib.quiver.Barbs 411 instance created 412 413 See Also 414 -------- 415 :func:`matplotlib.pyplot.barbs` 416 417 """ 418 # If plot_units specified, convert the data to those units 419 plotting_units = kwargs.pop('plot_units', None) 420 if plotting_units: 421 if hasattr(u, 'units') and hasattr(v, 'units'): 422 u = u.to(plotting_units) 423 v = v.to(plotting_units) 424 else: 425 raise ValueError('To convert to plotting units, units must be attached to ' 426 'u and v wind components.') 427 428 # Assemble array of x-locations in axes space 429 x = np.empty_like(pressure) 430 x.fill(xloc) 431 432 # Do barbs plot at this location 433 if c is not None: 434 b = self.ax.barbs(x, pressure, u, v, c, 435 transform=self.ax.get_yaxis_transform(which='tick2'), 436 clip_on=True, zorder=2, **kwargs) 437 else: 438 b = self.ax.barbs(x, pressure, u, v, 439 transform=self.ax.get_yaxis_transform(which='tick2'), 440 clip_on=True, zorder=2, **kwargs) 441 442 # Override the default clip box, which is the axes rectangle, so we can have 443 # barbs that extend outside. 444 ax_bbox = transforms.Bbox([[xloc - x_clip_radius, -y_clip_radius], 445 [xloc + x_clip_radius, 1.0 + y_clip_radius]]) 446 b.set_clip_box(transforms.TransformedBbox(ax_bbox, self.ax.transAxes)) 447 return b 448 449 def plot_dry_adiabats(self, t0=None, pressure=None, **kwargs): 450 r"""Plot dry adiabats. 451 452 Adds dry adiabats (lines of constant potential temperature) to the 453 plot. The default style of these lines is dashed red lines with an alpha 454 value of 0.5. These can be overridden using keyword arguments. 455 456 Parameters 457 ---------- 458 t0 : array_like, optional 459 Starting temperature values in Kelvin. If none are given, they will be 460 generated using the current temperature range at the bottom of 461 the plot. 462 pressure : array_like, optional 463 Pressure values to be included in the dry adiabats. If not 464 specified, they will be linearly distributed across the current 465 plotted pressure range. 466 kwargs 467 Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` 468 469 Returns 470 ------- 471 matplotlib.collections.LineCollection 472 instance created 473 474 See Also 475 -------- 476 :func:`~metpy.calc.thermo.dry_lapse` 477 :meth:`plot_moist_adiabats` 478 :class:`matplotlib.collections.LineCollection` 479 480 """ 481 # Remove old lines 482 if self.dry_adiabats: 483 self.dry_adiabats.remove() 484 485 # Determine set of starting temps if necessary 486 if t0 is None: 487 xmin, xmax = self.ax.get_xlim() 488 t0 = units.Quantity(np.arange(xmin, xmax + 1, 10), 'degC') 489 490 # Get pressure levels based on ylims if necessary 491 if pressure is None: 492 pressure = units.Quantity(np.linspace(*self.ax.get_ylim()), 'mbar') 493 494 # Assemble into data for plotting 495 t = dry_lapse(pressure, t0[:, np.newaxis], 496 units.Quantity(1000., 'mbar')).to(units.degC) 497 linedata = [np.vstack((ti.m, pressure.m)).T for ti in t] 498 499 # Add to plot 500 kwargs.setdefault('colors', 'r') 501 kwargs.setdefault('linestyles', 'dashed') 502 kwargs.setdefault('alpha', 0.5) 503 self.dry_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs)) 504 return self.dry_adiabats 505 506 def plot_moist_adiabats(self, t0=None, pressure=None, **kwargs): 507 r"""Plot moist adiabats. 508 509 Adds saturated pseudo-adiabats (lines of constant equivalent potential 510 temperature) to the plot. The default style of these lines is dashed 511 blue lines with an alpha value of 0.5. These can be overridden using 512 keyword arguments. 513 514 Parameters 515 ---------- 516 t0 : array_like, optional 517 Starting temperature values in Kelvin. If none are given, they will be 518 generated using the current temperature range at the bottom of 519 the plot. 520 pressure : array_like, optional 521 Pressure values to be included in the moist adiabats. If not 522 specified, they will be linearly distributed across the current 523 plotted pressure range. 524 kwargs 525 Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` 526 527 Returns 528 ------- 529 matplotlib.collections.LineCollection 530 instance created 531 532 See Also 533 -------- 534 :func:`~metpy.calc.thermo.moist_lapse` 535 :meth:`plot_dry_adiabats` 536 :class:`matplotlib.collections.LineCollection` 537 538 """ 539 # Remove old lines 540 if self.moist_adiabats: 541 self.moist_adiabats.remove() 542 543 # Determine set of starting temps if necessary 544 if t0 is None: 545 xmin, xmax = self.ax.get_xlim() 546 t0 = units.Quantity(np.concatenate((np.arange(xmin, 0, 10), 547 np.arange(0, xmax + 1, 5))), 'degC') 548 549 # Get pressure levels based on ylims if necessary 550 if pressure is None: 551 pressure = units.Quantity(np.linspace(*self.ax.get_ylim()), 'mbar') 552 553 # Assemble into data for plotting 554 t = moist_lapse(pressure, t0[:, np.newaxis], 555 units.Quantity(1000., 'mbar')).to(units.degC) 556 linedata = [np.vstack((ti.m, pressure.m)).T for ti in t] 557 558 # Add to plot 559 kwargs.setdefault('colors', 'b') 560 kwargs.setdefault('linestyles', 'dashed') 561 kwargs.setdefault('alpha', 0.5) 562 self.moist_adiabats = self.ax.add_collection(LineCollection(linedata, **kwargs)) 563 return self.moist_adiabats 564 565 def plot_mixing_lines(self, mixing_ratio=None, pressure=None, **kwargs): 566 r"""Plot lines of constant mixing ratio. 567 568 Adds lines of constant mixing ratio (isohumes) to the 569 plot. The default style of these lines is dashed green lines with an 570 alpha value of 0.8. These can be overridden using keyword arguments. 571 572 Parameters 573 ---------- 574 mixing_ratio : array_like, optional 575 Unitless mixing ratio values to plot. If none are given, default 576 values are used. 577 pressure : array_like, optional 578 Pressure values to be included in the isohumes. If not 579 specified, they will be linearly distributed across the current 580 plotted pressure range up to 600 mb. 581 kwargs 582 Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` 583 584 Returns 585 ------- 586 matplotlib.collections.LineCollection 587 instance created 588 589 See Also 590 -------- 591 :class:`matplotlib.collections.LineCollection` 592 593 """ 594 # Remove old lines 595 if self.mixing_lines: 596 self.mixing_lines.remove() 597 598 # Default mixing level values if necessary 599 if mixing_ratio is None: 600 mixing_ratio = np.array([0.0004, 0.001, 0.002, 0.004, 0.007, 0.01, 601 0.016, 0.024, 0.032]).reshape(-1, 1) 602 603 # Set pressure range if necessary 604 if pressure is None: 605 pressure = units.Quantity(np.linspace(600, max(self.ax.get_ylim())), 'mbar') 606 607 # Assemble data for plotting 608 td = dewpoint(vapor_pressure(pressure, mixing_ratio)) 609 linedata = [np.vstack((t.m, pressure.m)).T for t in td] 610 611 # Add to plot 612 kwargs.setdefault('colors', 'g') 613 kwargs.setdefault('linestyles', 'dashed') 614 kwargs.setdefault('alpha', 0.8) 615 self.mixing_lines = self.ax.add_collection(LineCollection(linedata, **kwargs)) 616 return self.mixing_lines 617 618 def shade_area(self, y, x1, x2=0, which='both', **kwargs): 619 r"""Shade area between two curves. 620 621 Shades areas between curves. Area can be where one is greater or less than the other 622 or all areas shaded. 623 624 Parameters 625 ---------- 626 y : array_like 627 1-dimensional array of numeric y-values 628 x1 : array_like 629 1-dimensional array of numeric x-values 630 x2 : array_like 631 1-dimensional array of numeric x-values 632 which : string 633 Specifies if `positive`, `negative`, or `both` areas are being shaded. 634 Will be overridden by where. 635 kwargs 636 Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection` 637 638 Returns 639 ------- 640 :class:`matplotlib.collections.PolyCollection` 641 642 See Also 643 -------- 644 :class:`matplotlib.collections.PolyCollection` 645 :func:`matplotlib.axes.Axes.fill_betweenx` 646 647 """ 648 fill_properties = {'positive': 649 {'facecolor': 'tab:red', 'alpha': 0.4, 'where': x1 > x2}, 650 'negative': 651 {'facecolor': 'tab:blue', 'alpha': 0.4, 'where': x1 < x2}, 652 'both': 653 {'facecolor': 'tab:green', 'alpha': 0.4, 'where': None}} 654 655 try: 656 fill_args = fill_properties[which] 657 fill_args.update(kwargs) 658 except KeyError: 659 raise ValueError(f'Unknown option for which: {which}') from None 660 661 arrs = y, x1, x2 662 663 if fill_args['where'] is not None: 664 arrs = arrs + (fill_args['where'],) 665 fill_args.pop('where', None) 666 667 fill_args['interpolate'] = True 668 669 arrs = _delete_masked_points(*arrs) 670 671 return self.ax.fill_betweenx(*arrs, **fill_args) 672 673 def shade_cape(self, pressure, t, t_parcel, **kwargs): 674 r"""Shade areas of Convective Available Potential Energy (CAPE). 675 676 Shades areas where the parcel is warmer than the environment (areas of positive 677 buoyancy. 678 679 Parameters 680 ---------- 681 pressure : array_like 682 Pressure values 683 t : array_like 684 Temperature values 685 dewpoint : array_like 686 Dewpoint values 687 t_parcel : array_like 688 Parcel path temperature values 689 limit_shading : bool 690 Eliminate shading below the LCL or above the EL, default is True 691 kwargs 692 Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection` 693 694 Returns 695 ------- 696 :class:`matplotlib.collections.PolyCollection` 697 698 See Also 699 -------- 700 :class:`matplotlib.collections.PolyCollection` 701 :func:`matplotlib.axes.Axes.fill_betweenx` 702 703 """ 704 return self.shade_area(pressure, t_parcel, t, which='positive', **kwargs) 705 706 def shade_cin(self, pressure, t, t_parcel, dewpoint=None, **kwargs): 707 r"""Shade areas of Convective INhibition (CIN). 708 709 Shades areas where the parcel is cooler than the environment (areas of negative 710 buoyancy). If `dewpoint` is passed in, negative area below the lifting condensation 711 level or above the equilibrium level is not shaded. 712 713 Parameters 714 ---------- 715 pressure : array_like 716 Pressure values 717 t : array_like 718 Temperature values 719 t_parcel : array_like 720 Parcel path temperature values 721 dewpoint : array_like 722 Dew point values, optional 723 kwargs 724 Other keyword arguments to pass to :class:`matplotlib.collections.PolyCollection` 725 726 Returns 727 ------- 728 :class:`matplotlib.collections.PolyCollection` 729 730 See Also 731 -------- 732 :class:`matplotlib.collections.PolyCollection` 733 :func:`matplotlib.axes.Axes.fill_betweenx` 734 735 """ 736 if dewpoint is not None: 737 lcl_p, _ = lcl(pressure[0], t[0], dewpoint[0]) 738 el_p, _ = el(pressure, t, dewpoint, t_parcel) 739 idx = np.logical_and(pressure > el_p, pressure < lcl_p) 740 else: 741 idx = np.arange(0, len(pressure)) 742 return self.shade_area(pressure[idx], t_parcel[idx], t[idx], which='negative', 743 **kwargs) 744 745 746@exporter.export 747class Hodograph: 748 r"""Make a hodograph of wind data. 749 750 Plots the u and v components of the wind along the x and y axes, respectively. 751 752 This class simplifies the process of creating a hodograph using matplotlib. 753 It provides helpers for creating a circular grid and for plotting the wind as a line 754 colored by another value (such as wind speed). 755 756 Attributes 757 ---------- 758 ax : `matplotlib.axes.Axes` 759 The underlying Axes instance used for all plotting 760 761 """ 762 763 def __init__(self, ax=None, component_range=80): 764 r"""Create a Hodograph instance. 765 766 Parameters 767 ---------- 768 ax : `matplotlib.axes.Axes`, optional 769 The `Axes` instance used for plotting 770 component_range : value 771 The maximum range of the plot. Used to set plot bounds and control the maximum 772 number of grid rings needed. 773 774 """ 775 if ax is None: 776 import matplotlib.pyplot as plt 777 self.ax = plt.figure().add_subplot(1, 1, 1) 778 else: 779 self.ax = ax 780 self.ax.set_aspect('equal', 'box') 781 self.ax.set_xlim(-component_range, component_range) 782 self.ax.set_ylim(-component_range, component_range) 783 784 # == sqrt(2) * max_range, which is the distance at the corner 785 self.max_range = 1.4142135 * component_range 786 787 def add_grid(self, increment=10., **kwargs): 788 r"""Add grid lines to hodograph. 789 790 Creates lines for the x- and y-axes, as well as circles denoting wind speed values. 791 792 Parameters 793 ---------- 794 increment : value, optional 795 The value increment between rings 796 kwargs 797 Other kwargs to control appearance of lines 798 799 See Also 800 -------- 801 :class:`matplotlib.patches.Circle` 802 :meth:`matplotlib.axes.Axes.axhline` 803 :meth:`matplotlib.axes.Axes.axvline` 804 805 """ 806 # Some default arguments. Take those, and update with any 807 # arguments passed in 808 grid_args = {'color': 'grey', 'linestyle': 'dashed'} 809 if kwargs: 810 grid_args.update(kwargs) 811 812 # Take those args and make appropriate for a Circle 813 circle_args = grid_args.copy() 814 color = circle_args.pop('color', None) 815 circle_args['edgecolor'] = color 816 circle_args['fill'] = False 817 818 self.rings = [] 819 for r in np.arange(increment, self.max_range, increment): 820 c = Circle((0, 0), radius=r, **circle_args) 821 self.ax.add_patch(c) 822 self.rings.append(c) 823 824 # Add lines for x=0 and y=0 825 self.yaxis = self.ax.axvline(0, **grid_args) 826 self.xaxis = self.ax.axhline(0, **grid_args) 827 828 @staticmethod 829 def _form_line_args(kwargs): 830 """Simplify taking the default line style and extending with kwargs.""" 831 def_args = {'linewidth': 3} 832 def_args.update(kwargs) 833 return def_args 834 835 def plot(self, u, v, **kwargs): 836 r"""Plot u, v data. 837 838 Plots the wind data on the hodograph. 839 840 Parameters 841 ---------- 842 u : array_like 843 u-component of wind 844 v : array_like 845 v-component of wind 846 kwargs 847 Other keyword arguments to pass to :meth:`matplotlib.axes.Axes.plot` 848 849 Returns 850 ------- 851 list[matplotlib.lines.Line2D] 852 lines plotted 853 854 See Also 855 -------- 856 :meth:`Hodograph.plot_colormapped` 857 858 """ 859 line_args = self._form_line_args(kwargs) 860 u, v = _delete_masked_points(u, v) 861 return self.ax.plot(u, v, **line_args) 862 863 def wind_vectors(self, u, v, **kwargs): 864 r"""Plot u, v data as wind vectors. 865 866 Plot the wind data as vectors for each level, beginning at the origin. 867 868 Parameters 869 ---------- 870 u : array_like 871 u-component of wind 872 v : array_like 873 v-component of wind 874 kwargs 875 Other keyword arguments to pass to :meth:`matplotlib.axes.Axes.quiver` 876 877 Returns 878 ------- 879 matplotlib.quiver.Quiver 880 arrows plotted 881 882 """ 883 quiver_args = {'units': 'xy', 'scale': 1} 884 quiver_args.update(**kwargs) 885 center_position = np.zeros_like(u) 886 return self.ax.quiver(center_position, center_position, 887 u, v, **quiver_args) 888 889 def plot_colormapped(self, u, v, c, intervals=None, colors=None, **kwargs): 890 r"""Plot u, v data, with line colored based on a third set of data. 891 892 Plots the wind data on the hodograph, but with a colormapped line. Takes a third 893 variable besides the winds (e.g. heights or pressure levels) and either a colormap to 894 color it with or a series of contour intervals and colors to create a colormap and 895 norm to control colormapping. The intervals must always be in increasing 896 order. For using custom contour intervals with height data, the function will 897 automatically interpolate to the contour intervals from the height and wind data, 898 as well as convert the input contour intervals from height AGL to MSL to work with the 899 provided heights. 900 901 Parameters 902 ---------- 903 u : array_like 904 u-component of wind 905 v : array_like 906 v-component of wind 907 c : array_like 908 data to use for colormapping (e.g. heights, pressure, wind speed) 909 intervals: array-like, optional 910 Array of intervals for c to use in coloring the hodograph. 911 colors: list, optional 912 Array of strings representing colors for the hodograph segments. 913 kwargs 914 Other keyword arguments to pass to :class:`matplotlib.collections.LineCollection` 915 916 Returns 917 ------- 918 matplotlib.collections.LineCollection 919 instance created 920 921 See Also 922 -------- 923 :meth:`Hodograph.plot` 924 925 """ 926 u, v, c = _delete_masked_points(u, v, c) 927 928 # Plotting a color segmented hodograph 929 if colors: 930 cmap = mcolors.ListedColormap(colors) 931 # If we are segmenting by height (a length), interpolate the contour intervals 932 if intervals.check('[length]'): 933 934 # Find any intervals not in the data and interpolate them 935 heights_min = np.nanmin(c) 936 heights_max = np.nanmax(c) 937 interpolation_heights = np.array([bound.m for bound in intervals 938 if bound not in c 939 and heights_min <= bound <= heights_max]) 940 interpolation_heights = units.Quantity(np.sort(interpolation_heights), 941 intervals.units) 942 interpolated_u, interpolated_v = interpolate_1d(interpolation_heights, c, u, v) 943 944 # Combine the interpolated data with the actual data 945 c = concatenate([c, interpolation_heights]) 946 u = concatenate([u, interpolated_u]) 947 v = concatenate([v, interpolated_v]) 948 sort_inds = np.argsort(c) 949 c = c[sort_inds] 950 u = u[sort_inds] 951 v = v[sort_inds] 952 953 # Unit conversion required for coloring of bounds/data in dissimilar units 954 # to work properly. 955 c = c.to_base_units() # TODO: This shouldn't be required! 956 intervals = intervals.to_base_units() 957 958 norm = mcolors.BoundaryNorm(intervals.magnitude, cmap.N) 959 cmap.set_over('none') 960 cmap.set_under('none') 961 kwargs['cmap'] = cmap 962 kwargs['norm'] = norm 963 line_args = self._form_line_args(kwargs) 964 965 # Plotting a continuously colored line 966 else: 967 line_args = self._form_line_args(kwargs) 968 969 # Do the plotting 970 lc = colored_line(u, v, c, **line_args) 971 self.ax.add_collection(lc) 972 return lc 973