Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 128 additions & 3 deletions src/metpy/plots/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from .patheffects import ColdFront, OccludedFront, StationaryFront, WarmFront
from .station_plot import StationPlot
from .text import scattertext, TextCollection
from ..calc import reduce_point_density, smooth_n_point, zoom_xarray
from ..calc import find_peaks, reduce_point_density, smooth_n_point, zoom_xarray
from ..package_tools import Exporter
from ..units import units

Expand Down Expand Up @@ -1032,7 +1032,7 @@ class ContourPlot(PlotScalar, ContourTraits, ValidationMixin):
black.

This trait can be set to any Matplotlib color
(https://matplotlib.org/3.1.0/gallery/color/named_colors.html)
(https://matplotlib.org/stable/gallery/color/named_colors.html)
"""

linewidth = Int(2)
Expand Down Expand Up @@ -1170,7 +1170,7 @@ class PlotVector(Plots2D):
black.

This trait can be set to any named color from
`Matplotlibs Colors <https://matplotlib.org/3.1.0/gallery/color/named_colors.html>`
`Matplotlibs Colors <https://matplotlib.org/stable/gallery/color/named_colors.html>`
"""

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


@exporter.export
class PlotExtrema(PlotScalar, MetPyHasTraits, ValidationMixin):
"""Plot maximum and/or minimum symbols and values of gridded datasets."""

peaks = List(default_value=['maxima'])
peaks.__doc__ = """A list of strings indicating which extrema to plot.
The default value is ['maxima'].

The only valid strings are 'maxima' and 'minima' for this attribute.

See Also
--------
metpy.calc.find_peaks
"""

peak_ratio = List(default_value=[2.0])
peak_ratio.__doc__ = """A list of float values for the inerquartile range ratio.
The default value is [2.0].

This ratio value is an optional setting for the find_peaks function that uses the
inner-quartile range to create a threshold for persistence of local peaks.
"""

symbol = List(default_value=['H'])
symbol.__doc__ = """A list of strings representing the extrema being plotted.
The default value is ['H'].

This can be set to any string you wish to plot at the extrema point. For example, use
'L' to signify a pressure minima.
"""

symbol_size = List(default_value=[20.0])
symbol_size.__doc__ = """A list of float values setting the size of the extrema symbol.
The default value is [20.0].

The value of the float will set the size of the symbol with larger values generating
a larger symbol.
"""

symbol_color = List(default_value=['black'])
symbol_color.__doc__ = """A list of strings representing the color of the symbol.
The default value is ['black'].

This trait can be set to any named color from
`Matplotlibs Colors <https://matplotlib.org/stable/gallery/color/named_colors.html>`
"""

plot_value = List(default_value=[False])
plot_value.__doc__ = """A list of booleans representing whether to plot the numeric
extrema integer value. The default value is [False].

This parameter controls plotting the numeric local maxima or minima value in
addition to the extrema symbol.
"""

text_size = List(default_value=[12])
text_size.__doc__ = """A list of float values setting the text size of the extrema value.
The default value is [12.0].
"""

text_location = List(default_value=['bottom'])
text_location.__doc__ = """A list of strings representing the vertical alignment for
plotting the extrema value text. The default value is ['bottom'].

The available options are 'baseline', 'bottom', 'center', 'center_baseline', 'top'.
"""

@observe('peak', 'peak_ratio', 'symbol', 'symbol_size', 'symbol_color', 'plot_value',
'text_size', 'text_location')
def _set_need_rebuild(self, _):
"""Handle changes to attributes that need to regenerate everything."""
# Because matplotlib doesn't let you just change these properties, we need
# to trigger a clear and re-call of scattertext()
self.clear()

def _build(self):
"""Build the raster plot by calling any plotting methods as necessary."""
x_like, y_like, imdata = self.plotdata

kwargs = plot_kwargs(imdata, self.mpl_args)

for i, extreme in enumerate(self.peaks):
peak_ratio = self.peak_ratio[i] if len(self.peak_ratio) > 1 else self.peak_ratio[0]

if extreme == 'minima':
extrema_y, extrema_x = find_peaks(imdata.values, maxima=False,
iqr_ratio=peak_ratio)
elif extreme == 'maxima':
extrema_y, extrema_x = find_peaks(imdata.values, iqr_ratio=peak_ratio)

plot_value = self.plot_value[i] if len(self.plot_value) > 1 else self.plot_value[0]

location = 'top' if plot_value else 'center'

symbol = self.symbol[i] if len(self.symbol) > 1 else self.symbol[0]

if len(self.symbol_color) > 1:
color = self.symbol_color[i]
else:
color = self.symbol_color[0]

if len(self.symbol_size) > 1:
symbol_size = self.symbol_size[i]
else:
symbol_size = self.symbol_size[0]

text_size = self.text_size[i] if len(self.text_size) > 1 else self.text_size[0]

if len(self.text_location) > 1:
text_loc = self.text_location[i]
else:
text_loc = self.text_location[0]

scattertext(self.parent.ax, x_like[extrema_x], y_like[extrema_y], symbol,
color=color, size=int(symbol_size),
verticalalignment=location, clip_on=True, **kwargs)

if plot_value:
scattertext(self.parent.ax, x_like[extrema_x], y_like[extrema_y],
imdata.values[extrema_y, extrema_x],
color=color, size=int(text_size),
verticalalignment=text_loc, formatter='.0f',
clip_on=True, **kwargs)


@exporter.export
class PlotObs(MetPyHasTraits, ValidationMixin):
"""The highest level class related to plotting observed surface and upperair data.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 67 additions & 2 deletions tests/plots/test_declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from metpy.io import GiniFile, parse_wpc_surface_bulletin
from metpy.io.metar import parse_metar_file
from metpy.plots import (ArrowPlot, BarbPlot, ContourPlot, FilledContourPlot, ImagePlot,
MapPanel, PanelContainer, PlotGeometry, PlotObs, PlotSurfaceAnalysis,
RasterPlot)
MapPanel, PanelContainer, PlotExtrema, PlotGeometry, PlotObs,
PlotSurfaceAnalysis, RasterPlot)
from metpy.testing import needs_cartopy, version_check
from metpy.units import units

Expand Down Expand Up @@ -1204,6 +1204,71 @@
return pc.figure


@pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.019)
@needs_cartopy
def test_declarative_extrema():
"""Test plotting gridded extrema points."""
data = xr.open_dataset(get_test_data('GFS_test.nc', as_file_obj=False))

Check warning

Code scanning / CodeQL

File is not always closed Warning test

File is opened but is not closed.

extrema = PlotExtrema()
extrema.data = data
extrema.level = 850 * units.hPa
extrema.field = 'Geopotential_height_isobaric'
extrema.peaks = ['minima', 'maxima']
extrema.symbol = ['L', 'H']
extrema.symbol_color = ['tab:red', 'tab:blue']
extrema.symbol_size = ['30', '25']
extrema.plot_value = [True, False]
extrema.text_size = [14, 10]
extrema.text_location = ['baseline']

panel = MapPanel()
panel.area = 'uslcc-'
panel.projection = 'lcc'
panel.layers = ['coastline', 'borders']
panel.plots = [extrema]

pc = PanelContainer()
pc.size = (8, 8)
pc.panels = [panel]
pc.draw()

return pc.figure


@pytest.mark.mpl_image_compare(remove_text=True, tolerance=0.019)
@needs_cartopy
def test_declarative_nam_extrema():
"""Test plotting gridded extrema points."""
data = xr.open_dataset(get_test_data('NAM_test.nc', as_file_obj=False))

Check warning

Code scanning / CodeQL

File is not always closed Warning test

File is opened but is not closed.

extrema = PlotExtrema()
extrema.data = data
extrema.level = 850 * units.hPa
extrema.field = 'Geopotential_height_isobaric'
extrema.peaks = ['minima']
extrema.peak_ratio = [3]
extrema.symbol = ['L']
extrema.symbol_color = ['red']
extrema.plot_value = [False]
extrema.text_location = ['bottom', 'baseline']

panel = MapPanel()
panel.area = 'uslcc'
panel.projection = 'lcc'
panel.layers = ['coastline', 'borders']
panel.plots = [extrema]

pc = PanelContainer()
pc.size = (8, 8)
pc.panels = [panel]
pc.draw()

extrema.symbol_color = ['black']

return pc.figure


@pytest.fixture()
def sample_obs():
"""Generate sample observational data for testing."""
Expand Down
Loading