Skip to content

Commit

Permalink
New PlotSurfaceAnalysis class to plot WPC surface analysis bulletins (#…
Browse files Browse the repository at this point in the history
…3580)

* new Plot_Bulletin class to plot wpc surface analysis bulletins

* added documentation

* edits from code review

* flake8 errors

* code climate errors

* code climate errors pt 2

* adding tests and fixed some errors

* added 'trough' to codespellignore

* codeclimate errors fixed

* Refactor to scattertext

Scattertext supports same config with
TextCollection under the hood, and has hack for
bbox clipping with cartopy. Fix style.

* fixing codespell error

* test plots

* remove klystron

---------

Co-authored-by: Drew Camron <[email protected]>
Co-authored-by: Drew Camron <[email protected]>
  • Loading branch information
3 people authored Aug 28, 2024
1 parent 92cb09c commit ddc198c
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 22 deletions.
18 changes: 18 additions & 0 deletions .codespellexclude
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
r'^(z|lv_|bottom_top|sigma|h(ei)?ght|altitude|depth|isobaric|pres|isotherm)'
| ``(thta, u, v, dx, dy, dim_order='yx')`` |
Changed signature from ``(thta, u, v, dx, dy, dim_order='yx')``
dpres = gempak.PRES.values
pres = 1
thta = 2
lambda grid: grid if grid.PARM in parameter else False,
col_head.SELV,
'SELV': col_head.SELV,
col_head.SELV,
row_head.SELV,
'SELV': row_head.SELV,
'SELV': col_head.SELV,
# GFS, NAM, RAP, or other gridded dataset (e.g., NARR).
# This attribute can be set to False if the vector components are grid relative (e.g., for NAM
components that are earth-relative. The primary exception is NAM output with wind
col_head.SELV,
row_head.SELV,
20 changes: 1 addition & 19 deletions .codespellignore
Original file line number Diff line number Diff line change
@@ -1,19 +1 @@
r'^(z|lv_|bottom_top|sigma|h(ei)?ght|altitude|depth|isobaric|pres|isotherm)'
| ``(thta, u, v, dx, dy, dim_order='yx')`` |
Changed signature from ``(thta, u, v, dx, dy, dim_order='yx')``
dpres = gempak.PRES.values
pres = 1
thta = 2
lambda grid: grid if grid.PARM in parameter else False,
col_head.SELV,
'SELV': col_head.SELV,
col_head.SELV,
row_head.SELV,
'SELV': row_head.SELV,
'SELV': col_head.SELV,
Klystron Warmup Integer*2 N/A 0 to 1 1 0=Normal, 1=Preheat 146
# GFS, NAM, RAP, or other gridded dataset (e.g., NARR).
# This attribute can be set to False if the vector components are grid relative (e.g., for NAM
components that are earth-relative. The primary exception is NAM output with wind
col_head.SELV,
row_head.SELV,
trough
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ extras = [

[tool.codespell]
skip = "*.tbl,*.ipynb,AUTHORS.txt,gempak.rst,.git,./staticdata,./docs/build,*.pdf,./talks"
exclude-file = ".codespellignore"
exclude-file = ".codespellexclude"
ignore-words = ".codespellignore"

[tool.doc8]
ignore-path = ["docs/build", "docs/api/generated", "docs/_templates", "docs/tutorials",
Expand Down
278 changes: 278 additions & 0 deletions src/metpy/plots/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from . import ctables, wx_symbols
from ._mpl import TextCollection
from .cartopy_utils import import_cartopy
from .patheffects import ColdFront, OccludedFront, StationaryFront, WarmFront
from .station_plot import StationPlot
from ..calc import reduce_point_density, smooth_n_point, zoom_xarray
from ..package_tools import Exporter
Expand Down Expand Up @@ -1988,3 +1989,280 @@ def _build(self):

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


@exporter.export
class PlotSurfaceAnalysis(MetPyHasTraits):
"""Plot Surface Analysis Features.
This class visualizes Surface Analysis features, including the parsed WPC Surface
Analysis bulletins processed by the `parse_wpc_surface_bulletin()` function.
"""

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
``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
'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.
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.
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.
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.
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.
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.
A single string (color name or hex code) used to plot trough lines. Default
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.
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.
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_markersize = Union([Int(), Float(), Unicode()], default_value=3, allow_none=True)
FRONT_markersize.__doc__ = """Size of symbols in front lines.
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, -1), allow_none=True)
strength_offset.__doc__ = """Offset between label of pressure system and its
corresponding strength.
Tuple representing the relative position of strength value with respect to label of
pressure system. Default value is (0,-1). Scaled by multiplying times 80% of
label_fontsize value.
"""

def _effect_map(self):
return {
'WARM': [WarmFront(size=self.FRONT_markersize, color=self.WARM_color)],
'COLD': [ColdFront(size=self.FRONT_markersize, color=self.COLD_color)],
'OCFNT': [OccludedFront(size=self.FRONT_markersize, color=self.OCFNT_color)],
'STNRY': [StationaryFront(size=self.FRONT_markersize,
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 'Surface Analysis Plot'

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
"""
if offset is None:
offset = tuple(x * self.label_fontsize * 0.8 for x in self.strength_offset)

self.parent.ax.scattertext([lon], [lat], [str(text)],
color=color,
loc=offset,
weight='demi',
size=int(self.label_fontsize * 0.7),
transform=ccrs.PlateCarree(),
clip_on=True)

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.scattertext([lon], [lat], [str(text)],
color=color,
loc=offset,
weight='demi',
size=self.label_fontsize,
transform=ccrs.PlateCarree(),
clip_on=True)

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
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.
Loading

0 comments on commit ddc198c

Please sign in to comment.