Skip to content

Commit

Permalink
fist pass declarative obs syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
kgoebber authored and dopplershift committed Dec 22, 2019
1 parent e9d67ac commit 5e4eb72
Show file tree
Hide file tree
Showing 11 changed files with 10,675 additions and 3 deletions.
67 changes: 67 additions & 0 deletions examples/plots/surface_declarative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright (c) 2019 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""
=========================================
Surface Analysis using Declarative Syntax
=========================================
The MetPy declarative syntax allows for a simplified interface to creating common
meteorological analyses including surface observation plots.
"""

########################################
from datetime import datetime, timedelta

import cartopy.crs as ccrs
import pandas as pd

from metpy.cbook import get_test_data
import metpy.plots as mpplots

########################################
# **Getting the data**
#
# In this example, data is originally from the Iowa State ASOS archive
# (https://mesonet.agron.iastate.edu/request/download.phtml) downloaded through a separate
# Python script. The data are pre-processed to determine sky cover and weather symbols from
# text output.

data = pd.read_csv(get_test_data('SFC_obs.csv', as_file_obj=False),
infer_datetime_format=True, parse_dates=['valid'])

########################################
# **Plotting the data**
#
# Use the declarative plotting interface to plot surface observations over the state of
# Georgia.

# Plotting the Observations using a 15 minute time window for surface observations
obs = mpplots.PlotObs()
obs.data = data
obs.time = datetime(1993, 3, 12, 13)
obs.time_window = timedelta(minutes=15)
obs.level = None
obs.fields = ['tmpf', 'dwpf', 'emsl', 'cloud_cover', 'wxsym']
obs.locations = ['NW', 'SW', 'NE', 'C', 'W']
obs.colors = ['red', 'green', 'black', 'black', 'blue']
obs.formats = [None, None, lambda v: format(10 * v, '.0f')[-3:], 'sky_cover',
'current_weather']
obs.vector_field = ('uwind', 'vwind')
obs.reduce_points = 1

# Add map features for the particular panel
panel = mpplots.MapPanel()
panel.layout = (1, 1, 1)
panel.area = 'ga'
panel.projection = ccrs.PlateCarree()
panel.layers = ['coastline', 'borders', 'states']
panel.plots = [obs]

# Collecting panels for complete figure
pc = mpplots.PanelContainer()
pc.size = (10, 10)
pc.panels = [panel]

# Showing the results
pc.show()
62 changes: 62 additions & 0 deletions examples/plots/upperair_declarative.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright (c) 2019 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""
=========================================
Surface Analysis using Declarative Syntax
=========================================
The MetPy declarative syntax allows for a simplified interface to creating common
meteorological analyses including surface observation plots.
"""

########################################
from datetime import datetime

import pandas as pd

from metpy.cbook import get_test_data
import metpy.plots as mpplots
from metpy.units import units


########################################
# **Getting the data**
#
# In this example, data is originally from the Iowa State Upper-air archive
# (https://mesonet.agron.iastate.edu/archive/raob/) available through a Siphon method.
# The data are pre-processed to attach latitude/lognitude locations for each RAOB site.

data = pd.read_csv(get_test_data('UPA_obs.csv', as_file_obj=False))

########################################
# **Plotting the data**
#
# Use the declarative plotting interface to create a CONUS upper-air map for 500 hPa

# Plotting the Observations
obs = mpplots.PlotObs()
obs.data = data
obs.time = datetime(1993, 3, 14, 0)
obs.level = 500 * units.hPa
obs.fields = ['temperature', 'dewpoint', 'height']
obs.locations = ['NW', 'SW', 'NE']
obs.formats = [None, None, lambda v: format(v, '.0f')[:3]]
obs.vector_field = ('u_wind', 'v_wind')
obs.reduce_points = 0

# Add map features for the particular panel
panel = mpplots.MapPanel()
panel.layout = (1, 1, 1)
panel.area = (-124, -72, 20, 53)
panel.projection = 'lcc'
panel.layers = ['coastline', 'borders', 'states', 'land', 'ocean']
panel.plots = [obs]

# Collecting panels for complete figure
pc = mpplots.PanelContainer()
pc.size = (15, 10)
pc.panels = [panel]

# Showing the results
pc.show()
240 changes: 239 additions & 1 deletion src/metpy/plots/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# SPDX-License-Identifier: BSD-3-Clause
"""Declarative plotting tools."""

from datetime import datetime
from datetime import datetime, timedelta

try:
import cartopy.crs as ccrs
Expand All @@ -16,6 +16,9 @@
Unicode, Union)

from . import ctables
from . import wx_symbols
from .station_plot import StationPlot
from ..calc import reduce_point_density
from ..package_tools import Exporter
from ..units import units

Expand Down Expand Up @@ -1262,3 +1265,238 @@ def _build(self):
u.values[wind_slice], v.values[wind_slice],
color=self.color, pivot=self.pivot, length=self.barblength,
transform=transform)


@exporter.export
class PlotObs(HasTraits):
"""The highest level class related to plotting observed surface and upperair data.
This class collects all common methods no matter whether plotting a upper-level or
surface data using station plots.
List of Traits:
* level
* time
* fields
* locations (optional)
* time_range (optional)
* formatters (optional)
* colors (optional)
* symbol_mapper (optional)
* vector_field (optional)
* vector_field_color (optional)
* reduce_points (optional)
"""

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

level = Union([Int(allow_none=True), Instance(units.Quantity)])
level.__doc__ = """The level of the field to be plotted.
This is a value with units to choose the desired plot level. For example, selecting the
850-hPa level, set this parameter to ``850 * units.hPa``. For surface data, parameter
must be set to `None`.
"""

time = Instance(datetime, allow_none=True)
time.__doc__ = """Set the valid time to be plotted as a datetime object.
If a forecast hour is to be plotted the time should be set to the valid future time, which
can be done using the `~datetime.datetime` and `~datetime.timedelta` objects
from the Python standard library.
"""

time_window = Instance(timedelta, default_value=timedelta(minutes=0), allow_none=True)
time_window.__doc__ = """Set a range to look for data to plot as a timedelta object.
If this parameter is set, it will subset the data provided to be within the time and plus
or minus the range value given. If there is more than one observation from a given station
then it will keep only the most recent one for plotting purposes. Default value is to have
no range. (optional)
"""

fields = List(Unicode())
fields.__doc__ = """Name of the scalar or symbol fields to be plotted.
List of parameters to be plotted around station plot (e.g., temperature, dewpoint, skyc).
"""

locations = List(default_value=['C'])
locations.__doc__ = """List of strings for scalar or symbol field plotting locations.
List of parameters locations for plotting parameters around the station plot (e.g.,
NW, NE, SW, SE, W, C). (optional)
"""

formats = List(default_value=[None])
formats.__doc__ = """List of the scalar and symbol field data formats. (optional)
List of scalar parameters formmaters or mapping values (if symbol) for plotting text and/or
symbols around the station plot (e.g., for pressure variable
```lambda v: format(10 * v, '.0f')[-3:]```).
For symbol mapping the following options are available to be put in as a string:
current_weather, sky_cover, low_clouds, mid_clouds, high_clouds, and pressure_tendency.
"""

colors = List(Unicode(), default_value=['black'])
colors.__doc__ = """List of the scalar and symbol field colors.
List of strings that represent the colors to be used for the variable being plotted.
(optional)
"""

vector_field = List(default_value=[None], allow_none=True)
vector_field.__doc__ = """List of the vector field to be plotted.
List of vector components to combined and plotted from the center of the station plot
(e.g., wind components). (optional)
"""

vector_field_color = Unicode('black', allow_none=True)
vector_field_color.__doc__ = """String color name to plot the vector. (optional)"""

reduce_points = Float(default_value=0)
reduce_points.__doc__ = """Float to reduce number of points plotted. (optional)"""

def clear(self):
"""Clear the plot.
Resets all internal state and sets need for redraw.
"""
if getattr(self, 'handle', None) is not None:
self.handle.ax.cla()
self.handle = None
self._need_redraw = True

@observe('parent')
def _parent_changed(self, _):
"""Handle setting the parent object for the plot."""
self.clear()

@observe('fields', 'level', 'time', 'vector_field', 'time_window')
def _update_data(self, _=None):
"""Handle updating the internal cache of data.
Responds to changes in various subsetting parameters.
"""
self._obsdata = None
self.clear()

# Can't be a Traitlet because notifications don't work with arrays for traits
# notification never happens
@property
def data(self):
"""Pandas dataframe that contains the fields to be plotted."""
return self._data

@data.setter
def data(self, val):
self._data = val
self._update_data()

@property
def name(self):
"""Generate a name for the plot."""
ret = ''
ret += ' and '.join(f for f in self.fields)
if self.level is not None:
ret += '@{:d}'.format(self.level)
return ret

@property
def obsdata(self):
"""Return the internal cached data."""
time_vars = ['valid', 'time', 'valid_time']
stn_vars = ['station', 'stn']
if getattr(self, '_obsdata', None) is None:
for dim_name in list(self.data):
if dim_name in time_vars:
dim_time = dim_name
elif dim_name in stn_vars:
dim_stn = dim_name
if self.level is not None:
level_subset = self.data.pressure == self.level.m
self._obsdata = self.data[level_subset]
else:
if self.time_window is not None:
time_slice = ((self.data[dim_time] >= (self.time - self.time_window))
& (self.data[dim_time] <= (self.time + self.time_window)))
data = self.data[time_slice].groupby(dim_stn).tail(1)
else:
data = self.data.groupby(dim_stn).tail(1)
self._obsdata = data
return self._obsdata

@property
def plotdata(self):
"""Return the data for plotting.
The data arrays, x coordinates, and y coordinates.
"""
plot_data = {}
for dim_name in list(self.obsdata):
if dim_name.find('lat') != -1:
lat = self.obsdata[dim_name]
elif dim_name.find('lon') != -1:
lon = self.obsdata[dim_name]
else:
plot_data[dim_name] = self.obsdata[dim_name]
return lon.values, lat.values, plot_data

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

@observe('colors', 'formats', 'locations', 'reduce_points', 'vector_field_color')
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 contour()
self.clear()

def _build(self):
"""Build the plot by calling needed plotting methods as necessary."""
lon, lat, data = self.plotdata

# Use the cartopy map projection to transform station locations to the map and
# then refine the number of stations plotted by setting a 300km radius
if self.parent._proj_obj == ccrs.PlateCarree():
scale = 1.
else:
scale = 100000.
point_locs = self.parent._proj_obj.transform_points(ccrs.PlateCarree(), lon, lat)
subset = reduce_point_density(point_locs, self.reduce_points * scale)

self.handle = StationPlot(self.parent.ax, lon[subset], lat[subset], clip_on=True,
transform=ccrs.PlateCarree(), fontsize=10)

for i, ob_type in enumerate(self.fields):
if len(self.locations) > 1:
location = self.locations[i]
else:
location = self.locations[0]
if len(self.colors) > 1:
color = self.colors[i]
else:
color = self.colors[0]
if self.formats[i] is not None:
mapper = getattr(wx_symbols, str(self.formats[i]), None)
if mapper is not None:
self.handle.plot_symbol(location, data[ob_type][subset],
mapper, color=color)
else:
self.handle.plot_parameter(location, data[ob_type][subset],
color=color, formatter=self.formats[i])
else:
self.handle.plot_parameter(location, data[ob_type][subset], color=color)
if self.vector_field[0] is not None:
self.handle.plot_barb(data[self.vector_field[0]][subset],
data[self.vector_field[1]][subset])
2 changes: 2 additions & 0 deletions src/metpy/static-data-manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ TDAL20191021021543V08.raw.gz db299c0f31f1396caddb92ed1517d30494a6f47ca994138f85c
Level3_Composite_dhr_1km_20180309_2225.gini 19fcc0179c9d3e87c462262ea817e87f52f60db4830314b8f936baa3b9817a44
NAM_test.nc 12338ad06d5bd223e99e2872b20a9c80d58af0c546731e4b00a6619adc247cd0
NHEM-MULTICOMP_1km_IR_20151208_2100.gini c144b29284aa915e6fd1b8f01939c656f2c72c3d7a9e0af5397f93067fe0d952
SFC_obs.csv 582d434442880f8414a555f22f7faf9b07b1170f390628e68c5c182e4431f426
UPA_obs.csv 106fa93db40f32cf7cb5e969e5dacb3d4e3387f7a3379ba90cc15149754fc640
WEST-CONUS_4km_WV_20151208_2200.gini 6851b49d20de2ee3e6fc0ef86e6c0f8f22170a6bd03bd6940b28c3ec09b8e7f6
barnes_r40_k100.npz a467b14872f4b9e7773c7583a61ad5ff70890e603f712e9a21b2c64fba9bd01c
barnes_test.npz 50870b92fe5cbeedfc70fb0c877b3d84b3e6ba2ba17ebdfe1c407d49e23a0555
Expand Down
Loading

0 comments on commit 5e4eb72

Please sign in to comment.