Skip to content

Commit 1c88b18

Browse files
committed
add declarative skewt plotting
1 parent 983c183 commit 1c88b18

File tree

7 files changed

+493
-27
lines changed

7 files changed

+493
-27
lines changed

src/metpy/plots/declarative.py

Lines changed: 329 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from . import ctables, wx_symbols
2121
from ._mpl import TextCollection
2222
from .cartopy_utils import import_cartopy
23+
from .skewt import SkewT
2324
from .station_plot import StationPlot
2425
from ..calc import reduce_point_density, smooth_n_point, zoom_xarray
2526
from ..package_tools import Exporter
@@ -627,8 +628,35 @@ def copy(self):
627628
return copy.copy(self)
628629

629630

631+
class PanelTraits(MetPyHasTraits):
632+
"""Represent common traits for panels."""
633+
634+
title = Unicode()
635+
title.__doc__ = """A string to set a title for the figure.
636+
637+
This trait sets a user-defined title that will plot at the top center of the figure.
638+
"""
639+
640+
title_fontsize = Union([Int(), Float(), Unicode()], allow_none=True, default_value=None)
641+
title_fontsize.__doc__ = """An integer or string value for the font size of the title of the
642+
figure.
643+
644+
This trait sets the font size for the title that will plot at the top center of the figure.
645+
Accepts size in points or relative size. Allowed relative sizes are those of Matplotlib:
646+
'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'.
647+
"""
648+
649+
plots = List(Any())
650+
plots.__doc__ = """A list of handles that represent the plots (e.g., `ContourPlot`,
651+
`FilledContourPlot`, `ImagePlot`, `SkewPlot`) to put on a given panel.
652+
653+
This trait collects the different plots, including contours and images, that are intended
654+
for a given panel.
655+
"""
656+
657+
630658
@exporter.export
631-
class MapPanel(Panel, ValidationMixin):
659+
class MapPanel(Panel, PanelTraits, ValidationMixin):
632660
"""Set figure related elements for an individual panel.
633661
634662
Parameters that need to be set include collecting all plotting types
@@ -650,14 +678,6 @@ class MapPanel(Panel, ValidationMixin):
650678
`matplotlib.figure.Figure.add_subplot`.
651679
"""
652680

653-
plots = List(Any())
654-
plots.__doc__ = """A list of handles that represent the plots (e.g., `ContourPlot`,
655-
`FilledContourPlot`, `ImagePlot`) to put on a given panel.
656-
657-
This trait collects the different plots, including contours and images, that are intended
658-
for a given panel.
659-
"""
660-
661681
_need_redraw = Bool(default_value=True)
662682

663683
area = Union([Unicode(), Tuple(Float(), Float(), Float(), Float())], allow_none=True,
@@ -713,21 +733,6 @@ class MapPanel(Panel, ValidationMixin):
713733
provided by user. Use `None` value for 'ocean', 'lakes', 'rivers', and 'land'.
714734
"""
715735

716-
title = Unicode()
717-
title.__doc__ = """A string to set a title for the figure.
718-
719-
This trait sets a user-defined title that will plot at the top center of the figure.
720-
"""
721-
722-
title_fontsize = Union([Int(), Float(), Unicode()], allow_none=True, default_value=None)
723-
title_fontsize.__doc__ = """An integer or string value for the font size of the title of the
724-
figure.
725-
726-
This trait sets the font size for the title that will plot at the top center of the figure.
727-
Accepts size in points or relative size. Allowed relative sizes are those of Matplotlib:
728-
'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'.
729-
"""
730-
731736
@validate('area')
732737
def _valid_area(self, proposal):
733738
"""Check that proposed string or tuple is valid and turn string into a tuple extent."""
@@ -921,6 +926,123 @@ def copy(self):
921926
return copy.copy(self)
922927

923928

929+
@exporter.export
930+
class SkewtPanel(PanelTraits, Panel):
931+
"""A class to collect skewt plots and set complete figure related settings (e.g., size)."""
932+
933+
parent = Instance(PanelContainer, allow_none=True)
934+
935+
ylimits = Tuple(Int(), Int(), default_value=(1000, 100), allow_none=True)
936+
ylimits.__doc__ = """A tuple of y-axis limits to plot the skew-T.
937+
938+
Order is in higher pressure to lower pressure."""
939+
940+
xlimits = Tuple(Int(), Int(), default_value=(-40, 40), allow_none=True)
941+
xlimits.__doc__ = """A tuple of x-axis limits to plot the skew-T.
942+
943+
Order is lower temperature to higher temperature."""
944+
945+
ylabel = Unicode(default_value='pressure [hPa]')
946+
ylabel.__doc__ = """A string to plot for the y-axis label.
947+
948+
Defaults to 'pressure [hPa]'"""
949+
950+
xlabel = Unicode(default_value='temperature [\N{DEGREE SIGN}C]')
951+
xlabel.__doc__ = """A string to plot for the y-axis label.
952+
953+
Defaults to 'temperature [C]'"""
954+
955+
@observe('plots')
956+
def _plots_changed(self, change):
957+
"""Handle when our collection of plots changes."""
958+
for plot in change.new:
959+
plot.parent = self
960+
plot.observe(self.refresh, names=('_need_redraw'))
961+
self._need_redraw = True
962+
963+
@observe('parent')
964+
def _parent_changed(self, _):
965+
"""Handle when the parent is changed."""
966+
self.ax = None
967+
968+
@property
969+
def ax(self):
970+
"""Get the :class:`matplotlib.axes.Axes` to draw on.
971+
972+
Creates a new instance if necessary.
973+
974+
"""
975+
# If we haven't actually made an instance yet, make one with the right size and
976+
# map projection.
977+
if getattr(self, '_ax', None) is None:
978+
self._ax = SkewT(self.parent.figure, rotation=45)
979+
980+
return self._ax
981+
982+
@ax.setter
983+
def ax(self, val):
984+
"""Set the :class:`matplotlib.axes.Axes` to draw on.
985+
986+
Clears existing state as necessary.
987+
988+
"""
989+
if getattr(self, '_ax', None) is not None:
990+
self._ax.cla()
991+
self._ax = val
992+
993+
def refresh(self, changed):
994+
"""Refresh the drawing if necessary."""
995+
self._need_redraw = changed.new
996+
997+
def draw(self):
998+
"""Draw the panel."""
999+
# Only need to run if we've actually changed.
1000+
if self._need_redraw:
1001+
1002+
skew = self.ax
1003+
1004+
# Set the extent as appropriate based on the limits.
1005+
xmin, xmax = self.xlimits
1006+
ymax, ymin = self.ylimits
1007+
skew.ax.set_xlim(xmin, xmax)
1008+
skew.ax.set_ylim(ymax, ymin)
1009+
skew.ax.set_xlabel(self.xlabel)
1010+
skew.ax.set_ylabel(self.ylabel)
1011+
1012+
# Draw all of the plots.
1013+
for p in self.plots:
1014+
with p.hold_trait_notifications():
1015+
p.draw()
1016+
1017+
skew.plot_labeled_skewt_lines()
1018+
1019+
# Use the set title or generate one.
1020+
title = self.title or ',\n'.join(plot.name for plot in self.plots)
1021+
skew.ax.set_title(title, fontsize=self.title_fontsize)
1022+
self._need_redraw = False
1023+
1024+
def __copy__(self):
1025+
"""Return a copy of this SkewPanel."""
1026+
# Create new, blank instance of MapPanel
1027+
cls = self.__class__
1028+
obj = cls.__new__(cls)
1029+
1030+
# Copy each attribute from current MapPanel to new MapPanel
1031+
for name in self.trait_names():
1032+
# The 'plots' attribute is a list.
1033+
# A copy must be made for each plot in the list.
1034+
if name == 'plots':
1035+
obj.plots = [copy.copy(plot) for plot in self.plots]
1036+
else:
1037+
setattr(obj, name, getattr(self, name))
1038+
1039+
return obj
1040+
1041+
def copy(self):
1042+
"""Return a copy of the panel."""
1043+
return copy.copy(self)
1044+
1045+
9241046
class SubsetTraits(MetPyHasTraits):
9251047
"""Represent common traits for subsetting data."""
9261048

@@ -2205,3 +2327,186 @@ def _build(self):
22052327

22062328
# Finally, draw the label
22072329
self._draw_label(label, lon, lat, fontcolor, fontoutline, offset)
2330+
2331+
2332+
@exporter.export
2333+
class SkewtPlot(MetPyHasTraits, ValidationMixin):
2334+
"""A class to set plot charactersitics of skewt data."""
2335+
2336+
temperature_variable = List(Unicode())
2337+
temperature_variable.__doc__ = """A list of string names for plotting variables from dictinary-like object.
2338+
2339+
No order in particular is needed, however, to shade cape or cin the order of temperature,
2340+
dewpoint temperature, parcel temperature is required."""
2341+
2342+
vertical_variable = Unicode()
2343+
vertical_variable.__doc__ = """A string with the vertical variable name (e.g., 'pressure').
2344+
2345+
"""
2346+
2347+
linecolor = List(Unicode(default_value='black'))
2348+
linecolor.__doc__ = """A list of color names corresponding to the parameters in `temperature_variables`.
2349+
2350+
A list of the same length as `temperature_variables` is preferred, otherwise, colors will
2351+
repeat. The default value is 'black'."""
2352+
2353+
linestyle = List(Unicode(default_value='solid'))
2354+
linestyle.__doc__ = """A list of line style names corresponding to the parameters in `temperature_variables`.
2355+
2356+
A list of the same length as `temperature_variables` is preferred, otherwise, colors will
2357+
repeat. The default value is 'solid'."""
2358+
2359+
linewidth = List(Union([Int(), Float()]), default_value=[1])
2360+
linewidth.__doc__ = """A list of linewidth values corresponding to the parameters in `temperature_variables`.
2361+
2362+
A list of the same length as `temperature_variables` is preferred, otherwise, colors will
2363+
repeat. The default value is 1."""
2364+
2365+
shade_cape = Bool(default_value=False)
2366+
shade_cape.__doc__ = """A boolean (True/False) on whether to shade the CAPE for the sounding.
2367+
2368+
This parameter uses the default settings from MetPy for plotting CAPE. In order to shade
2369+
CAPE, the `temperature_variables` attribute must be in the order of temperature, dewpoint
2370+
temperature, parcel temperature. The default value is `False`."""
2371+
2372+
shade_cin = Bool(default_value=False)
2373+
shade_cin.__doc__ = """A boolean (True/False) on whether to shade the CIN for the sounding.
2374+
2375+
This parameter uses the default settings from MetPy for plotting CIN using the dewpoint,
2376+
so only the CIN between the surface and the LFC is filled. In order to shade CIN, the
2377+
`temperature_variables` attribute must be in the order of temperature, dewpoint
2378+
temperature, parcel temperature. The default value is `False`."""
2379+
2380+
wind_barb_variables = List(default_value=[None], allow_none=True)
2381+
wind_barb_variables.__doc__ = """A list of string names of the u- and v-components of the wind.
2382+
2383+
This attribute requires two string names in the order u-component, v-component for those
2384+
respective variables stored in the dictionary-like object."""
2385+
2386+
wind_barb_color = Unicode('black', allow_none=True)
2387+
wind_barb_color.__doc__ = """A string declaring the name of the color to plot the wind barbs.
2388+
2389+
The default value is 'black'."""
2390+
2391+
wind_barb_length = Int(default_value=7, allow_none=True)
2392+
wind_barb_length.__doc__ = """An integer value for defining the size of the wind barbs.
2393+
2394+
The default value is 7."""
2395+
2396+
wind_barb_skip = Int(default_value=1)
2397+
wind_barb_skip.__doc__ = """An integer value for skipping the plotting of wind barbs.
2398+
2399+
The default value is 1 (no skipping)."""
2400+
2401+
wind_barb_position = Float(default_value=1.0)
2402+
wind_barb_position.__doc__ = """A float value for defining location of the wind barbs on the plot.
2403+
2404+
The float value describes the location in figure space. The default value is 1.0."""
2405+
2406+
parent = Instance(Panel)
2407+
_need_redraw = Bool(default_value=True)
2408+
2409+
def clear(self):
2410+
"""Clear the plot.
2411+
2412+
Resets all internal state and sets need for redraw.
2413+
2414+
"""
2415+
if getattr(self, 'handle', None) is not None:
2416+
self.handle.ax.cla()
2417+
self.handle = None
2418+
self._need_redraw = True
2419+
2420+
@observe('parent')
2421+
def _parent_changed(self, _):
2422+
"""Handle setting the parent object for the plot."""
2423+
self.clear()
2424+
2425+
@observe('temperature_variable', 'vertical_variable', 'wind_barb_variables')
2426+
def _update_data(self, _=None):
2427+
"""Handle updating the internal cache of data.
2428+
2429+
Responds to changes in various subsetting parameters.
2430+
2431+
"""
2432+
self._xydata = None
2433+
self.clear()
2434+
2435+
# Can't be a Traitlet because notifications don't work with arrays for traits
2436+
# notification never happens
2437+
@property
2438+
def data(self):
2439+
"""Dictionary-like data that contains the fields to be plotted."""
2440+
return self._data
2441+
2442+
@data.setter
2443+
def data(self, val):
2444+
self._data = val
2445+
self._update_data()
2446+
2447+
@property
2448+
def name(self):
2449+
"""Generate a name for the plot."""
2450+
ret = ''
2451+
ret += ' and '.join([self.x_variable])
2452+
return ret
2453+
2454+
@property
2455+
def xydata(self, var):
2456+
"""Return the internal cached data."""
2457+
if getattr(self, '_xydata', None) is None:
2458+
# Use a copy of data so we retain all of the original data passed in unmodified
2459+
self._xydata = self.data
2460+
return self._xydata[var]
2461+
2462+
def draw(self):
2463+
"""Draw the plot."""
2464+
if self._need_redraw:
2465+
if getattr(self, 'handle', None) is None:
2466+
self._build()
2467+
self._need_redraw = False
2468+
2469+
@observe('linecolor', 'linewidth', 'linestyle', 'wind_barb_color', 'wind_barb_length',
2470+
'wind_barb_position', 'wind_barb_skip', 'shade_cape', 'shade_cin')
2471+
def _set_need_rebuild(self, _):
2472+
"""Handle changes to attributes that need to regenerate everything."""
2473+
# Because matplotlib doesn't let you just change these properties, we need
2474+
# to trigger a clear and re-call of contour()
2475+
self.clear()
2476+
2477+
def _build(self):
2478+
"""Build the plot by calling needed plotting methods as necessary."""
2479+
data = self.data
2480+
y = data[self.vertical_variable]
2481+
if len(self.temperature_variable) != len(self.linewidth):
2482+
self.linewidth *= len(self.temperature_variable)
2483+
if len(self.temperature_variable) != len(self.linecolor):
2484+
self.linecolor *= len(self.temperature_variable)
2485+
if len(self.temperature_variable) != len(self.linestyle):
2486+
self.linestyle *= len(self.temperature_variable)
2487+
for i in range(len(self.temperature_variable)):
2488+
x = data[self.temperature_variable[i]]
2489+
2490+
self.parent.ax.plot(y, x, self.linecolor[i], linestyle=self.linestyle[i],
2491+
linewidth=self.linewidth[i])
2492+
2493+
if self.wind_barb_variables[0] is not None:
2494+
u = data[self.wind_barb_variables[0]]
2495+
v = data[self.wind_barb_variables[1]]
2496+
barb_skip = slice(None, None, self.wind_barb_skip)
2497+
self.parent.ax.plot_barbs(y[barb_skip], u[barb_skip], v[barb_skip],
2498+
y_clip_radius=0, xloc=self.wind_barb_position)
2499+
2500+
if self.shade_cape:
2501+
self.parent.ax.shade_cape(data[self.vertical_variable],
2502+
data[self.temperature_variable[0]],
2503+
data[self.temperature_variable[2]])
2504+
if self.shade_cin:
2505+
self.parent.ax.shade_cin(data[self.vertical_variable],
2506+
data[self.temperature_variable[0]],
2507+
data[self.temperature_variable[2]],
2508+
data[self.temperature_variable[1]])
2509+
2510+
def copy(self):
2511+
"""Return a copy of the plot."""
2512+
return copy.copy(self)

0 commit comments

Comments
 (0)