Skip to content

Commit 94f6764

Browse files
committed
adds extrema plotting to declarative
1 parent cb85c10 commit 94f6764

File tree

4 files changed

+193
-5
lines changed

4 files changed

+193
-5
lines changed

src/metpy/plots/declarative.py

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .patheffects import ColdFront, OccludedFront, StationaryFront, WarmFront
2424
from .station_plot import StationPlot
2525
from .text import scattertext, TextCollection
26-
from ..calc import reduce_point_density, smooth_n_point, zoom_xarray
26+
from ..calc import find_peaks, reduce_point_density, smooth_n_point, zoom_xarray
2727
from ..package_tools import Exporter
2828
from ..units import units
2929

@@ -1032,7 +1032,7 @@ class ContourPlot(PlotScalar, ContourTraits, ValidationMixin):
10321032
black.
10331033
10341034
This trait can be set to any Matplotlib color
1035-
(https://matplotlib.org/3.1.0/gallery/color/named_colors.html)
1035+
(https://matplotlib.org/stable/gallery/color/named_colors.html)
10361036
"""
10371037

10381038
linewidth = Int(2)
@@ -1170,7 +1170,7 @@ class PlotVector(Plots2D):
11701170
black.
11711171
11721172
This trait can be set to any named color from
1173-
`Matplotlibs Colors <https://matplotlib.org/3.1.0/gallery/color/named_colors.html>`
1173+
`Matplotlibs Colors <https://matplotlib.org/stable/gallery/color/named_colors.html>`
11741174
"""
11751175

11761176
@observe('field')
@@ -1385,6 +1385,131 @@ def _build(self):
13851385
self.parent.ax.quiverkey(self.handle, labelcolor=self.color, **key_kwargs)
13861386

13871387

1388+
@exporter.export
1389+
class PlotExtrema(PlotScalar, MetPyHasTraits, ValidationMixin):
1390+
"""Plot maximum and/or minimum symbols and values of gridded datasets."""
1391+
1392+
peaks = List(default_value=['maxima'])
1393+
peaks.__doc__ = """A list of strings indicating which extrema to plot.
1394+
The default value is ['maxima'].
1395+
1396+
The only valid strings are 'maxima' and 'minima' for this attribute.
1397+
1398+
See Also
1399+
--------
1400+
metpy.calc.find_peaks
1401+
"""
1402+
1403+
peak_ratio = List(default_value=[2.0])
1404+
peak_ratio.__doc__ = """A list of float values for the inerquartile range ratio.
1405+
The default value is [2.0].
1406+
1407+
This ratio value is an optional setting for the find_peaks function that uses the
1408+
inner-quartile range to create a threshold for persistence of local peaks.
1409+
"""
1410+
1411+
symbol = List(default_value=['H'])
1412+
symbol.__doc__ = """A list of strings representing the extrema being plotted.
1413+
The default value is ['H'].
1414+
1415+
This can be set to any string you wish to plot at the extrema point. For example, use
1416+
'L' to signify a pressure minima.
1417+
"""
1418+
1419+
symbol_size = List(default_value=[20.0])
1420+
symbol_size.__doc__ = """A list of float values setting the size of the extrema symbol.
1421+
The default value is [20.0].
1422+
1423+
The value of the float will set the size of the symbol with larger values generating
1424+
a larger symbol.
1425+
"""
1426+
1427+
symbol_color = List(default_value=['black'])
1428+
symbol_color.__doc__ = """A list of strings representing the color of the symbol.
1429+
The default value is ['black'].
1430+
1431+
This trait can be set to any named color from
1432+
`Matplotlibs Colors <https://matplotlib.org/stable/gallery/color/named_colors.html>`
1433+
"""
1434+
1435+
plot_value = List(default_value=[False])
1436+
plot_value.__doc__ = """A list of booleans representing whether to plot the numeric
1437+
extrema integer value. The default value is [False].
1438+
1439+
This parameter controls plotting the numeric local maxima or minima value in
1440+
addition to the extrema symbol.
1441+
"""
1442+
1443+
text_size = List(default_value=[12])
1444+
text_size.__doc__ = """A list of float values setting the text size of the extrema value.
1445+
The default value is [12.0].
1446+
"""
1447+
1448+
text_location = List(default_value=['bottom'])
1449+
text_location.__doc__ = """A list of strings representing the vertical alignment for
1450+
plotting the extrema value text. The default value is ['bottom'].
1451+
1452+
The available options are 'baseline', 'bottom', 'center', 'center_baseline', 'top'.
1453+
"""
1454+
1455+
@observe('extrema', 'symbol', 'symbol_size', 'plot_value', 'symbol_color', 'text_size',
1456+
'text_location')
1457+
def _set_need_rebuild(self, _):
1458+
"""Handle changes to attributes that need to regenerate everything."""
1459+
# Because matplotlib doesn't let you just change these properties, we need
1460+
# to trigger a clear and re-call of quiver()
1461+
self.clear()
1462+
1463+
def _build(self):
1464+
"""Build the raster plot by calling any plotting methods as necessary."""
1465+
x_like, y_like, imdata = self.plotdata
1466+
1467+
kwargs = plot_kwargs(imdata, self.mpl_args)
1468+
1469+
for i, extreme in enumerate(self.peaks):
1470+
peak_ratio = self.peak_ratio[i] if len(self.peak_ratio) > 1 else self.peak_ratio[0]
1471+
1472+
if extreme == 'minima':
1473+
extrema_y, extrema_x = find_peaks(imdata.values, maxima=False,
1474+
iqr_ratio=peak_ratio)
1475+
elif extreme == 'maxima':
1476+
extrema_y, extrema_x = find_peaks(imdata.values, iqr_ratio=peak_ratio)
1477+
1478+
plot_value = self.plot_value[i] if len(self.plot_value) > 1 else self.plot_value[0]
1479+
1480+
location = 'top' if plot_value else 'center'
1481+
1482+
symbol = self.symbol[i] if len(self.symbol) > 1 else self.symbol[0]
1483+
1484+
if len(self.symbol_color) > 1:
1485+
color = self.symbol_color[i]
1486+
else:
1487+
color = self.symbol_color[0]
1488+
1489+
if len(self.symbol_size) > 1:
1490+
symbol_size = self.symbol_size[i]
1491+
else:
1492+
symbol_size = self.symbol_size[0]
1493+
1494+
text_size = self.text_size[i] if len(self.text_size) > 1 else self.text_size[0]
1495+
1496+
if len(self.text_location) > 1:
1497+
text_loc = self.text_location[i]
1498+
else:
1499+
text_loc = self.text_location[0]
1500+
1501+
scattertext(self.parent.ax, x_like[extrema_x], y_like[extrema_y], symbol,
1502+
color=color, size=int(symbol_size),
1503+
verticalalignment=location, clip_on=True, **kwargs)
1504+
1505+
if plot_value:
1506+
scattertext(self.parent.ax, x_like[extrema_x], y_like[extrema_y],
1507+
imdata.values[extrema_y, extrema_x],
1508+
color=color, size=int(text_size),
1509+
verticalalignment=text_loc, formatter='.0f',
1510+
clip_on=True, **kwargs)
1511+
1512+
13881513
@exporter.export
13891514
class PlotObs(MetPyHasTraits, ValidationMixin):
13901515
"""The highest level class related to plotting observed surface and upperair data.
65.1 KB
Loading
56.3 KB
Loading

tests/plots/test_declarative.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from metpy.io import GiniFile, parse_wpc_surface_bulletin
2121
from metpy.io.metar import parse_metar_file
2222
from metpy.plots import (ArrowPlot, BarbPlot, ContourPlot, FilledContourPlot, ImagePlot,
23-
MapPanel, PanelContainer, PlotGeometry, PlotObs, PlotSurfaceAnalysis,
24-
RasterPlot)
23+
MapPanel, PanelContainer, PlotExtrema, PlotGeometry, PlotObs,
24+
PlotSurfaceAnalysis, RasterPlot)
2525
from metpy.testing import needs_cartopy, version_check
2626
from metpy.units import units
2727

@@ -1204,6 +1204,69 @@ def test_declarative_barb_gfs_knots():
12041204
return pc.figure
12051205

12061206

1207+
@pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.019)
1208+
@needs_cartopy
1209+
def test_declarative_extrema():
1210+
"""Test plotting gridded extrema points."""
1211+
data = xr.open_dataset(get_test_data('GFS_test.nc', as_file_obj=False))
1212+
1213+
extrema = PlotExtrema()
1214+
extrema.data = data
1215+
extrema.level = 850 * units.hPa
1216+
extrema.field = 'Geopotential_height_isobaric'
1217+
extrema.peaks = ['minima', 'maxima']
1218+
extrema.symbol = ['L', 'H']
1219+
extrema.symbol_color = ['tab:red', 'tab:blue']
1220+
extrema.symbol_size = ['30', '25']
1221+
extrema.plot_value = [True, False]
1222+
extrema.text_size = [14, 10]
1223+
extrema.text_location = ['baseline']
1224+
1225+
panel = MapPanel()
1226+
panel.area = 'uslcc-'
1227+
panel.projection = 'lcc'
1228+
panel.layers = ['coastline', 'borders']
1229+
panel.plots = [extrema]
1230+
1231+
pc = PanelContainer()
1232+
pc.size = (8, 8)
1233+
pc.panels = [panel]
1234+
pc.draw()
1235+
1236+
return pc.figure
1237+
1238+
1239+
@pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.005)
1240+
@needs_cartopy
1241+
def test_declarative_nam_extrema():
1242+
"""Test plotting gridded extrema points."""
1243+
data = xr.open_dataset(get_test_data('NAM_test.nc', as_file_obj=False))
1244+
1245+
extrema = PlotExtrema()
1246+
extrema.data = data
1247+
extrema.level = 850 * units.hPa
1248+
extrema.field = 'Geopotential_height_isobaric'
1249+
extrema.peaks = ['minima']
1250+
extrema.peak_ratio = [3]
1251+
extrema.symbol = ['L']
1252+
extrema.symbol_color = ['red']
1253+
extrema.plot_value = [False]
1254+
extrema.text_location = ['bottom', 'baseline']
1255+
1256+
panel = MapPanel()
1257+
panel.area = 'uslcc'
1258+
panel.projection = 'lcc'
1259+
panel.layers = ['coastline', 'borders']
1260+
panel.plots = [extrema]
1261+
1262+
pc = PanelContainer()
1263+
pc.size = (8, 8)
1264+
pc.panels = [panel]
1265+
pc.draw()
1266+
1267+
return pc.figure
1268+
1269+
12071270
@pytest.fixture()
12081271
def sample_obs():
12091272
"""Generate sample observational data for testing."""

0 commit comments

Comments
 (0)