Source code for act.plotting.geodisplay

"""
Stores the class for GeographicPlotDisplay.

"""


import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from .plot import Display

try:
    import cartopy.crs as ccrs
    import cartopy.feature as cfeature
    from cartopy.io import img_tiles

    CARTOPY_AVAILABLE = True
except ImportError:
    CARTOPY_AVAILABLE = False


[docs]class GeographicPlotDisplay(Display): """ A class for making geographic tracer plot of aircraft, ship or other moving platform plot. This is inherited from the :func:`act.plotting.Display` class and has therefore has the same attributes as that class. See :func:`act.plotting.Display` for more information. There are no additional attributes or parameters to this class. In order to create geographic plots, ACT needs the Cartopy package to be installed on your system. More information about Cartopy go here:https://scitools.org.uk/cartopy/docs/latest/ . """ def __init__(self, ds, ds_name=None, **kwargs): if not CARTOPY_AVAILABLE: raise ImportError( 'Cartopy needs to be installed on your ' 'system to make geographic display plots.' ) super().__init__(ds, ds_name, **kwargs) if self.fig is None: self.fig = plt.figure(**kwargs)
[docs] def geoplot( self, data_field=None, lat_field='lat', lon_field='lon', dsname=None, cbar_label=None, title=None, projection=None, plot_buffer=0.08, img_tile=None, img_tile_args={}, tile=8, cartopy_feature=None, cmap='rainbow', text=None, gridlines=True, **kwargs, ): """ Creates a latitude and longitude plot of a time series data set with data values indicated by color and described with a colorbar. Latitude values must be in degree north (-90 to 90) and longitude must be in degree east (-180 to 180). Parameters ---------- data_field : str Name of data field in the dataset to plot. lat_field : str Name of latitude field in the dataset to use. lon_field : str Name of longitude field in the dataset to use. dsname : str or None The name of the datastream to plot. Set to None to make ACT attempt to automatically determine this. cbar_label : str Label to use with colorbar. If set to None will attempt to create label from long_name and units. title : str Plot title. projection : cartopy.crs object Project to use on plot. See https://scitools.org.uk/cartopy/docs/latest/reference/projections.html?highlight=projections plot_buffer : float Buffer to add around data on plot in lat and lon dimension. img_tile : str Image to use for the plot background. Set to None to not use background image. For all image background types, see: https://scitools.org.uk/cartopy/docs/v0.16/cartopy/io/img_tiles.html Default is None. img_tile_args : dict Keyword arguments for the chosen img_tile. These arguments can be found for the corresponding img_tile here: https://scitools.org.uk/cartopy/docs/v0.16/cartopy/io/img_tiles.html Default is an empty dictionary. tile : int Tile zoom to use with background image. Higher number indicates more resolution. A value of 8 is typical for a normal sonde plot. cartopy_feature : list of str or str Cartopy feature to add to plot. cmap : str Color map to use for colorbar. text : dictionary Dictionary of {text:[lon,lat]} to add to plot. Can have more than one set of text to add. gridlines : boolean Use latitude and longitude gridlines. **kwargs : keyword arguments Any other keyword arguments that will be passed into :func:`matplotlib.pyplot.scatter` when the figure is made. See the matplotlib documentation for further details on what keyword arguments are available. Returns ------- ax : matplotlib axis handle The matplotlib axis handle of the plot. """ if dsname is None and len(self._ds.keys()) > 1: raise ValueError( 'You must choose a datastream when there are 2 ' 'or more datasets in the GeographicPlotDisplay ' 'object.' ) elif dsname is None: dsname = list(self._ds.keys())[0] if data_field is None: raise ValueError('You must enter the name of the data ' 'to be plotted.') if projection is None: if CARTOPY_AVAILABLE: projection = ccrs.PlateCarree() # Extract data from the dataset try: lat = self._ds[dsname][lat_field].values except KeyError: raise ValueError( ( 'You will need to provide the name of the ' "field if not '{}' to use for latitude " 'data.' ).format(lat_field) ) try: lon = self._ds[dsname][lon_field].values except KeyError: raise ValueError( ( 'You will need to provide the name of the ' "field if not '{}' to use for longitude " 'data.' ).format(lon_field) ) # Set up metadata information for display on plot if cbar_label is None: try: cbar_label = ( self._ds[dsname][data_field].attrs['long_name'] + ' (' + self._ds[dsname][data_field].attrs['units'] + ')' ) except KeyError: cbar_label = data_field lat_limits = [np.nanmin(lat), np.nanmax(lat)] lon_limits = [np.nanmin(lon), np.nanmax(lon)] box_size = np.max([np.abs(np.diff(lat_limits)), np.abs(np.diff(lon_limits))]) bx_buf = box_size * plot_buffer lat_center = np.sum(lat_limits) / 2.0 lon_center = np.sum(lon_limits) / 2.0 lat_limits = [ lat_center - box_size / 2.0 - bx_buf, lat_center + box_size / 2.0 + bx_buf, ] lon_limits = [ lon_center - box_size / 2.0 - bx_buf, lon_center + box_size / 2.0 + bx_buf, ] data = self._ds[dsname][data_field].values # Create base plot projection ax = plt.axes(projection=projection) plt.subplots_adjust(left=0.01, right=0.99, bottom=0.05, top=0.93) ax.set_extent([lon_limits[0], lon_limits[1], lat_limits[0], lat_limits[1]], crs=projection) if title is None: try: dim = list(self._ds[dsname][data_field].dims) ts = pd.to_datetime(str(self._ds[dsname][dim[0]].values[0])) date = ts.strftime('%Y-%m-%d') time_str = ts.strftime('%H:%M:%S') plt.title(' '.join([dsname, 'at', date, time_str])) except NameError: plt.title(dsname) else: plt.title(title) if img_tile is not None: tiler = getattr(img_tiles, img_tile)(**img_tile_args) ax.add_image(tiler, tile) colorbar_map = None if cmap is not None: colorbar_map = matplotlib.colormaps.get_cmap(cmap) sc = ax.scatter(lon, lat, c=data, cmap=colorbar_map, **kwargs) cbar = plt.colorbar(sc) cbar.ax.set_ylabel(cbar_label) if cartopy_feature is not None: if isinstance(cartopy_feature, str): cartopy_feature = [cartopy_feature] cartopy_feature = [ii.upper() for ii in cartopy_feature] if 'STATES' in cartopy_feature: ax.add_feature(cfeature.STATES.with_scale('10m')) if 'LAND' in cartopy_feature: ax.add_feature(cfeature.LAND) if 'OCEAN' in cartopy_feature: ax.add_feature(cfeature.OCEAN) if 'COASTLINE' in cartopy_feature: ax.add_feature(cfeature.COASTLINE) if 'BORDERS' in cartopy_feature: ax.add_feature(cfeature.BORDERS, linestyle=':') if 'LAKES' in cartopy_feature: ax.add_feature(cfeature.LAKES, alpha=0.5) if 'RIVERS' in cartopy_feature: ax.add_feature(cfeature.RIVERS) if text is not None: for label, location in text.items(): ax.plot(location[0], location[1], marker='*', color='black') ax.text(location[0], location[1], label, color='black') if gridlines: if projection == ccrs.PlateCarree() or projection == ccrs.Mercator: gl = ax.gridlines( crs=projection, draw_labels=True, linewidth=1, color='gray', alpha=0.5, linestyle='--', ) gl.top_labels = False gl.left_labels = True gl.bottom_labels = True gl.right_labels = False gl.xlabel_style = {'size': 6, 'color': 'gray'} gl.ylabel_style = {'size': 6, 'color': 'gray'} else: # Labels are only currently supported for PlateCarree and Mercator gl = ax.gridlines( draw_labels=False, linewidth=1, color='gray', alpha=0.5, linestyle='--', ) return ax