Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New PlotSurfaceAnalysis class to plot WPC surface analysis bulletins #3580

Merged
merged 15 commits into from
Aug 28, 2024
272 changes: 272 additions & 0 deletions src/metpy/plots/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@

from . import ctables, wx_symbols
from ._mpl import TextCollection
from metpy.plots import (ColdFront, OccludedFront, StationaryFront,
StationPlot, WarmFront)
dcamron marked this conversation as resolved.
Show resolved Hide resolved
from .cartopy_utils import import_cartopy
from .station_plot import StationPlot

Check failure on line 26 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 F811 Redefinition of unused `StationPlot` from line 24 Raw Output: src/metpy/plots/declarative.py:26:27: F811 Redefinition of unused `StationPlot` from line 24
from ..calc import reduce_point_density, smooth_n_point, zoom_xarray
from ..package_tools import Exporter
from ..units import units
Expand Down Expand Up @@ -1986,3 +1988,273 @@

# Finally, draw the label
self._draw_label(label, lon, lat, fontcolor, fontoutline, offset)


class PlotBulletin(MetPyHasTraits):
dcamron marked this conversation as resolved.
Show resolved Hide resolved
"""Plot previously parsed WPC Surface Analysis Bulletins with optional customization."""
dcamron marked this conversation as resolved.
Show resolved Hide resolved

parent = Instance(Panel)
_need_redraw = Bool(default_value=True)

geometry = Instance(collections.abc.Iterable, allow_none=False)
geometry.__doc__ = """A collection of Shapely objects to plot.

A collection of Shapely objects, such as the 'geometry' column from a bulletin parsed
with `parse_wpc_surface_bulletin()`. Acceptable Shapely objects are

Check failure on line 2003 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W291 Trailing whitespace Raw Output: src/metpy/plots/declarative.py:2003:72: W291 Trailing whitespace
``shapely.LineString``, and ``shapely.Point``.
"""

feature = Union([Instance(collections.abc.Iterable), Unicode()],
allow_none=False)
feature.__doc__ = """Collection of names of features to be plotted.

Collection of strings, each corresponding one-to-one with geometries, such as the

Check failure on line 2011 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W291 Trailing whitespace Raw Output: src/metpy/plots/declarative.py:2011:86: W291 Trailing whitespace
'features' column from a bulletin parsed with `parse_wpc_surface_bulletin()`.
Acceptable feature names include: 'HIGH', 'LOW', 'WARM', 'COLD', 'OCFNT', 'STNRY', 'TROF'.
"""

strength = Union([Instance(collections.abc.Iterable), Float()], default_value=[],
allow_none=True)
strength.__doc__ = """Collection of strengths corresponding to pressure systems.

Collection of floats, each corresponding one-to-one with pressure system features. Such
as the 'strength' column from a bulletin parsed with `parse_wpc_surface_bulletin()`.
"""

HIGH_color = Union([Unicode()], default_value='blue', allow_none=True)
HIGH_color.__doc__ = """Color for plotting high-pressure systems.

Check failure on line 2026 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W293 Blank line contains whitespace Raw Output: src/metpy/plots/declarative.py:2026:1: W293 Blank line contains whitespace
A single string (color name or hex code) used to plot label of high-pressure system and
their strength, if provided. Default value is 'blue'.
"""

LOW_color = Union([Unicode()], default_value='red', allow_none=True)
LOW_color.__doc__ = """Color for plotting low-pressure systems.

Check failure on line 2033 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W293 Blank line contains whitespace Raw Output: src/metpy/plots/declarative.py:2033:1: W293 Blank line contains whitespace
A single string (color name or hex code) used to plot label of low-pressure system and
their strength, if provided. Default value is 'red'.
"""

WARM_color = Union([Unicode()], default_value='red', allow_none=True)
WARM_color.__doc__ = """Color for plotting warm fronts.

Check failure on line 2040 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W293 Blank line contains whitespace Raw Output: src/metpy/plots/declarative.py:2040:1: W293 Blank line contains whitespace
A single string (color name or hex code) used to plot warm fronts. Default
color is 'red', which is used by `WarmFront()` class. `WARM_color` alternates
with `COLD_color` to plot stationary fronts.
"""

COLD_color = Union([Unicode()], default_value='blue', allow_none=True)
COLD_color.__doc__ = """Color for plotting cold fronts.

Check failure on line 2048 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W293 Blank line contains whitespace Raw Output: src/metpy/plots/declarative.py:2048:1: W293 Blank line contains whitespace
A single string (color name or hex code) used to plot cold fronts. Default
color is 'blue', which is used by `ColdFront()` class. `COLD_color` alternates
with `WARM_color` to plot stationary fronts.
"""

OCFNT_color = Union([Unicode()], default_value='purple', allow_none=True)
OCFNT_color.__doc__ = """Color for plotting occluded fronts.

Check failure on line 2056 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W293 Blank line contains whitespace Raw Output: src/metpy/plots/declarative.py:2056:1: W293 Blank line contains whitespace
A single string (color name or hex code) used to plot Occluded fronts. Default
color is 'purple', which is used by `OccludedFront()` class.
"""

TROF_color = Union([Unicode()], default_value='darkorange', allow_none=True)
TROF_color.__doc__ = """Color for plotting trough lines.

Check failure on line 2062 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[codespell] reported by reviewdog 🐶 trough ==> through Raw Output: ./src/metpy/plots/declarative.py:2062: trough ==> through

Check failure on line 2063 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[ruff] reported by reviewdog 🐶 W293 Blank line contains whitespace Raw Output: src/metpy/plots/declarative.py:2063:1: W293 Blank line contains whitespace
A single string (color name or hex code) used to plot trough lines. Default

Check failure on line 2064 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[codespell] reported by reviewdog 🐶 trough ==> through Raw Output: ./src/metpy/plots/declarative.py:2064: trough ==> through
color is 'darkorange'.
"""

HIGH_label = Union([Unicode()], default_value='H', allow_none=True)
HIGH_label.__doc__ = """Label used to plot high-pressure systems.

Single string used as marker to plot high-pressure systems. Default value is 'H'.
"""

LOW_label = Union([Unicode()], default_value='L', allow_none=True)
LOW_label.__doc__ = """Label used to plot low-pressure systems.

Single string used as marker to plot low-pressure systems. Default value is 'L'.
"""

TROF_linestyle = Union([Unicode()], default_value='dashed',
allow_none=True)
TROF_linestyle.__doc__ = """Linestyle of Trough lines.

Check failure on line 2082 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[codespell] reported by reviewdog 🐶 Trough ==> Through Raw Output: ./src/metpy/plots/declarative.py:2082: Trough ==> Through

Single string, default value is 'dashed'.
Accept matplotlib linestyles: 'solid', 'dotted', 'dashdot'.
"""

label_fontsize = Union([Int(), Float(), Unicode()], default_value=10, allow_none=True)
label_fontsize.__doc__ = """Font sizes of pressure systems labels.

Accepts size in points or relative size. Allowed relative sizes are those of Matplotlib:
'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'.
"""

TROF_linewidth = Union([Float()], default_value=2,
allow_none=True)
TROF_linewidth.__doc__ = """Stroke width for trough lines.

Check failure on line 2097 in src/metpy/plots/declarative.py

View workflow job for this annotation

GitHub Actions / Flake8

[codespell] reported by reviewdog 🐶 trough ==> through Raw Output: ./src/metpy/plots/declarative.py:2097: trough ==> through

A single integer or floating point value representing the size of the stroke width.
"""

FRONT_linewidth = Union([Float()], default_value=1,
allow_none=True)
TROF_linewidth.__doc__ = """Stroke width for front lines.

A single floating point value representing the size of the stroke width.
"""

FRONT_marker_size = Union([Int(), Float(), Unicode()], default_value=3, allow_none=True)
FRONT_marker_size.__doc__ = """Size of symbols in front lines.
dcamron marked this conversation as resolved.
Show resolved Hide resolved

Accepts size in points or relative size. Default value is 3. Allowed relative sizes are those
of Matplotlib: 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'.
"""

strength_offset = Union([Tuple()], default_value=(0,-6), allow_none=True)
dcamron marked this conversation as resolved.
Show resolved Hide resolved
strength_offset.__doc__ = """Offset between label of pressure system and its corresponding strength.

Tuple representing the position of strength value with respect to label of pressure system.
Default value is (0,-6).
"""

def _effect_map(self):
return {
'WARM': [WarmFront(size=self.FRONT_marker_size, color=self.WARM_color)],
'COLD': [ColdFront(size=self.FRONT_marker_size, color=self.COLD_color)],
'OCFNT': [OccludedFront(size=self.FRONT_marker_size, color=self.OCFNT_color)],
'STNRY': [StationaryFront(size=self.FRONT_marker_size, colors=(self.WARM_color, self.COLD_color))],
'TROF': None
}


def _color_map(self):
return {
'HIGH' : self.HIGH_color,
'LOW' : self.LOW_color,
'TROF' : self.TROF_color
}

def _linewidth_map(self):
return {
'WARM': self.FRONT_linewidth,
'COLD': self.FRONT_linewidth,
'OCFNT': self.FRONT_linewidth,
'STNRY': self.FRONT_linewidth,
'TROF': self.TROF_linewidth
}

def _label_map(self):
return {
'HIGH' : self.HIGH_label,
'LOW' : self.LOW_label
}

@property
def name(self):
"""Generate a name for the plot."""
# Unlike Plots2D and PlotObs, there are no other attributes (such as 'fields' or
# 'levels') from which to name the plot. A generic name is returned here in case the
# user does not provide their own title, in which case MapPanel.draw() looks here.
return 'WPC Bulletin Plot'
dcamron marked this conversation as resolved.
Show resolved Hide resolved

def _draw_strengths(self, text, lon, lat, color, offset=None):
"""Draw strengths in the plot.

Parameters
----------
text : str
The strength's value
lon : float
Longitude at which to position the label
lat : float
Latitude at which to position the label
color : str
Name or hex code for the color of the text
offset : tuple (default: (0, 0))
A tuple containing the x- and y-offset of the label, respectively
"""
import math
offset = self.strength_offset if offset is None else offset
self.parent.ax.add_artist(TextCollection([lon], [lat], [str(text)],
va='center',
ha='center',
color=color,
offset=offset,
weight='demi',
size = math.floor(self.label_fontsize * 0.7),
transform=ccrs.PlateCarree()))

def _draw_labels(self, text, lon, lat, color, offset=(0,0)):
"""Draw labels in the plot.

Parameters
----------
text : str
The label's text
lon : float
Longitude at which to position the label
lat : float
Latitude at which to position the label
color : str
Name or hex code for the color of the text
offset : tuple (default: (0, 0))
A tuple containing the x- and y-offset of the label, respectively
"""
self.parent.ax.add_artist(TextCollection([lon], [lat], [str(text)],
va='center',
ha='center',
color=color,
offset=offset,
weight='demi',
size=self.label_fontsize,
transform=ccrs.PlateCarree()))

def draw(self):
"""Draw the plot."""
if self._need_redraw:
if getattr(self, 'handles', None) is None:
self._build()
self._need_redraw = False

def copy(self):
"""Return a copy of the plot."""
return copy.copy(self)

def _build(self):
"""Build the plot by calling needed plotting methods as necessary."""
from shapely.geometry import (LineString, Point)

# Ensure strength is a valid iterable
strengths = self.strength if len(self.strength) > 0 else cycle([None])

# Map plotting parameters
effect_map = self._effect_map()
color_map = self._color_map()
linewidth_map = self._linewidth_map()
label_map = self._label_map()

# Each Shapely object is plotted separately with its corresponding strength
# and customizable parameters
for geo_obj, strengthvalues, feature in zip(
self.geometry, strengths, self.feature):
kwargs = self.mpl_args.copy()
# Plot the Shapely object with the appropriate method and style
if isinstance(geo_obj, (LineString)):
kwargs.setdefault('linewidths', linewidth_map[feature])
kwargs.setdefault('facecolor', 'none')
kwargs.setdefault('crs', ccrs.PlateCarree())
kwargs.setdefault('path_effects', effect_map[feature])
if feature=='TROF':
kwargs.setdefault('edgecolor', color_map[feature])
kwargs.setdefault('linestyle', self.TROF_linestyle)
self.parent.ax.add_geometries([geo_obj], **kwargs)
elif isinstance(geo_obj, Point):
kwargs.setdefault('color', color_map[feature])
lon, lat = geo_obj.coords[0]
self._draw_labels(label_map[feature],lon,lat, **kwargs)
# Plot strengths if provided
if strengthvalues is not None:
self._draw_strengths(strengthvalues, lon, lat, **kwargs)
Loading