Source code for act.plotting.windrosedisplay

"""
Stores the class for WindRoseDisplay.

"""

import warnings

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

# Import Local Libs
from ..utils import datetime_utils as dt_utils
from .plot import Display


[docs]class WindRoseDisplay(Display): """ A class for handing wind rose plots. 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. Examples -------- To create a WindRoseDisplay object, simply do: .. code-block :: python sonde_ds = act.io.arm.read_arm_netcdf('sonde_data.nc') WindDisplay = act.plotting.WindRoseDisplay(sonde_ds, figsize=(8,10)) """ def __init__(self, ds, subplot_shape=(1,), ds_name=None, **kwargs): super().__init__(ds, subplot_shape, ds_name, subplot_kw=dict(projection='polar'), **kwargs)
[docs] def set_thetarng(self, trng=(0.0, 360.0), subplot_index=(0,)): """ Sets the theta range of the wind rose plot. Parameters ---------- trng : 2-tuple The range (in degrees). subplot_index : 2-tuple The index of the subplot to set the degree range of. """ if self.axes is not None: self.axes[subplot_index].set_thetamin(trng[0]) self.axes[subplot_index].set_thetamax(trng[1]) self.trng = trng else: raise RuntimeError('Axes must be initialized before' + ' changing limits!')
[docs] def set_rrng(self, rrng, subplot_index=(0,)): """ Sets the range of the radius of the wind rose plot. Parameters ---------- rrng : 2-tuple The range for the plot radius (in %). subplot_index : 2-tuple The index of the subplot to set the radius range of. """ if self.axes is not None: self.axes[subplot_index].set_rmin(rrng[0]) self.axes[subplot_index].set_rmax(rrng[1]) self.rrng = rrng else: raise RuntimeError('Axes must be initialized before' + ' changing limits!')
[docs] def plot( self, dir_field, spd_field, dsname=None, subplot_index=(0,), cmap=None, set_title=None, num_dirs=20, spd_bins=None, tick_interval=3, legend_loc=0, legend_bbox=None, legend_title=None, calm_threshold=1.0, **kwargs, ): """ Makes the wind rose plot from the given dataset. Parameters ---------- dir_field : str The name of the field representing the wind direction (in degrees). spd_field : str The name of the field representing the wind speed. dsname : str The name of the datastream to plot from. Set to None to let ACT automatically try to determine this. subplot_index : 2-tuple The index of the subplot to place the plot on. cmap : str or matplotlib colormap The name of the matplotlib colormap to use. set_title : str The title of the plot. num_dirs : int The number of directions to split the wind rose into. spd_bins : 1D array-like The bin boundaries to sort the wind speeds into. tick_interval : int The interval (in %) for the ticks on the radial axis. legend_loc : int Legend location using matplotlib legend code legend_bbox : tuple Legend bounding box coordinates legend_title : string Legend title calm_threshold : float Winds below this threshold are considered to be calm. **kwargs : keyword arguments Additional keyword arguments will be passed into :func:plt.bar Returns ------- ax : matplotlib axis handle The matplotlib axis handle corresponding to 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 TimeSeriesDisplay ' 'object.' ) elif dsname is None: dsname = list(self._ds.keys())[0] # Get data and dimensions dir_data = self._ds[dsname][dir_field].values spd_data = self._ds[dsname][spd_field].values # Get the current plotting axis, add day/night background and plot data if self.fig is None: self.fig = plt.figure() if self.axes is None: self.axes = np.array([plt.axes(projection='polar')]) self.fig.add_axes(self.axes[0]) if spd_bins is None: spd_bins = np.linspace(0, np.nanmax(spd_data), 10) # Make the bins so that 0 degrees N is in the center of the first bin # We need to wrap around deg_width = 360.0 / num_dirs dir_bins_mid = np.linspace(0.0, 360.0 - 3 * deg_width / 2.0, num_dirs) wind_hist = np.zeros((num_dirs, len(spd_bins) - 1)) for i in range(num_dirs): if i == 0: with warnings.catch_warnings(): warnings.filterwarnings('ignore', 'invalid value encountered in.*') the_range = np.logical_or( dir_data < deg_width / 2.0, dir_data > 360.0 - deg_width / 2.0 ) else: with warnings.catch_warnings(): warnings.filterwarnings('ignore', 'invalid value encountered in.*') the_range = np.logical_and( dir_data >= dir_bins_mid[i] - deg_width / 2, dir_data <= dir_bins_mid[i] + deg_width / 2, ) hist, bins = np.histogram(spd_data[the_range], spd_bins) wind_hist[i] = hist wind_hist = wind_hist / np.sum(wind_hist) * 100 mins = np.deg2rad(dir_bins_mid) # Do the first level if 'units' in self._ds[dsname][spd_field].attrs.keys(): units = self._ds[dsname][spd_field].attrs['units'] else: units = '' the_label = '%3.1f' % spd_bins[0] + '-' + '%3.1f' % spd_bins[1] + ' ' + units our_cmap = matplotlib.colormaps.get_cmap(cmap) our_colors = our_cmap(np.linspace(0, 1, len(spd_bins))) ax = self.axes[subplot_index] bars = [ ax.bar( mins, wind_hist[:, 0], bottom=0, label=the_label, width=0.8 * np.deg2rad(deg_width), color=our_colors[0], **kwargs, ) ] for i in range(1, len(spd_bins) - 1): the_label = '%3.1f' % spd_bins[i] + '-' + '%3.1f' % spd_bins[i + 1] + ' ' + units # Changing the bottom to be a sum of the previous speeds so that # it positions it correctly - Adam Theisen bars.append( ax.bar( mins, wind_hist[:, i], label=the_label, bottom=np.sum(wind_hist[:, :i], axis=1), width=0.8 * np.deg2rad(deg_width), color=our_colors[i], **kwargs, ) ) ax.legend(loc=legend_loc, bbox_to_anchor=legend_bbox, title=legend_title) ax.set_theta_zero_location('N') ax.set_theta_direction(-1) # Add an annulus with text stating % of time calm pct_calm = np.sum(spd_data <= calm_threshold) / len(spd_data) * 100 ax.set_rorigin(-2.5) ax.annotate('%3.2f%%\n calm' % pct_calm, xy=(0, -2.5), ha='center', va='center') # Set the ticks to be nice numbers tick_max = tick_interval * round(np.nanmax(np.cumsum(wind_hist, axis=1)) / tick_interval) rticks = np.arange(0, tick_max, tick_interval) rticklabels = [('%d' % x + '%') for x in rticks] ax.set_rticks(rticks) ax.set_yticklabels(rticklabels) # Set Title if set_title is None: set_title = ' '.join( [ dsname, 'on', dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]), ] ) ax.set_title(set_title) self.axes[subplot_index] = ax return ax
[docs] def plot_data( self, dir_field, spd_field, data_field, dsname=None, subplot_index=(0,), plot_type='Line', line_color=None, set_title=None, num_dirs=30, num_data_bins=30, calm_threshold=1.0, line_plot_calc='mean', clevels=30, contour_type='count', cmap=None, **kwargs, ): """ Makes a data rose plot in line or boxplot form from the given data. Parameters ---------- dir_field : str The name of the field representing the wind direction (in degrees). spd_field : str The name of the field representing the wind speed. data_field : str Name of the field to plot. Default is to plot mean values. dsname : str The name of the datastream to plot from. Set to None to let ACT automatically try to determine this. subplot_index : 2-tuple The index of the subplot to place the plot on. plot_type : str Type of plot to create. Defaults to a line plot but the full options include 'line', 'contour', and 'boxplot' line_color : str Color to use for the line set_title : str The title of the plot. num_dirs : int The number of directions to split the wind rose into. num_data_bins : int The number of bins to use for data processing if doing a contour plot calm_threshold : float Winds below this threshold are considered to be calm. line_plot_calc : str What values to display for the line plot. Defaults to 'mean', but other options are 'median' and 'stdev' clevels : int Number of contour levels to plot contour_type : str Type of contour plot to do. Default is 'count' which displays a heatmap of where values are occuring most along with wind directions The other option is 'mean' which will do a wind direction x wind speed plot with the contours of the mean values for each wind dir/speed. num_data_bins will be used for number of wind speed bins cmap : str or matplotlib colormap The name of the matplotlib colormap to use. **kwargs : keyword arguments Additional keyword arguments will be passed into :func:plt.bar Returns ------- ax : matplotlib axis handle The matplotlib axis handle corresponding to 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 TimeSeriesDisplay ' 'object.' ) elif dsname is None: dsname = list(self._ds.keys())[0] # Get data and dimensions # Throw out calm winds for the analysis ds = self._ds[dsname] ds = ds.where(ds[spd_field] >= calm_threshold) dir_data = ds[dir_field].values data = ds[data_field].values # Set the bins dir_bins_mid = np.linspace(0.0, 360.0, num_dirs + 1) # Run through the data and bin based on the wind direction and plot type arr = [] bins = [] for i, d in enumerate(dir_bins_mid): if i < len(dir_bins_mid) - 1: idx = np.where((dir_data > d) & (dir_data <= dir_bins_mid[i + 1]))[0] bins.append(d + (dir_bins_mid[i + 1] - d) / 2.0) else: idx = np.where((dir_data > d) & (dir_data <= 360.0))[0] bins.append(d + (360.0 - d) / 2.0) if plot_type == 'line': if line_plot_calc == 'mean': arr.append(np.nanmean(data[idx])) plot_type_str = 'Mean of' elif line_plot_calc == 'median': arr.append(np.nanmedian(data[idx])) plot_type_str = 'Median of' elif line_plot_calc == 'stdev': plot_type_str = 'Standard Deviation of' arr.append(np.nanstd(data[idx])) else: raise ValueError('Please pick an available option') elif plot_type == 'boxplot': arr.append(data[idx]) # Plot data for each plot type if plot_type == 'line': # Add the first values to the end of the array to have a # complete circle bins.append(bins[0]) arr.append(arr[0]) self.axes[subplot_index].plot(np.deg2rad(bins), arr, **kwargs) elif plot_type == 'boxplot': # Plot boxplot self.axes[subplot_index].boxplot( arr, positions=np.deg2rad(bins), showmeans=False, **kwargs ) if bins[-1] == 360: bins[-1] = 0 self.axes[subplot_index].xaxis.set_ticklabels(np.ceil(bins)) plot_type_str = 'Boxplot of' elif plot_type == 'contour': # Calculate a histogram to plot out a contour for if contour_type == 'count': idx = np.where((~np.isnan(dir_data)) & (~np.isnan(data)))[0] hist, xedges, yedges = np.histogram2d( dir_data[idx], data[idx], bins=[num_dirs, num_data_bins] ) hist = np.insert(hist, -1, hist[0], axis=0) cplot = self.axes[subplot_index].contourf( np.deg2rad(xedges), yedges[0:-1], np.transpose(hist), cmap=cmap, levels=clevels, **kwargs, ) plot_type_str = 'Heatmap of' cbar = self.fig.colorbar(cplot, ax=self.axes[subplot_index]) cbar.ax.set_ylabel('Count') elif contour_type == 'mean': # Produce direction (x-axis) and speed (y-axis) plots displaying the mean # as the contours. spd_data = ds[spd_field].values spd_bins = np.linspace(0, ds[spd_field].max().values, num_data_bins + 1) spd_bins = np.insert(spd_bins, 1, calm_threshold) # Set up an array and cycle through the data, binning them by speed/direction mean_data = np.zeros([len(bins), len(spd_bins)]) for i in range(len(bins) - 1): for j in range(len(spd_bins)): if j < len(spd_bins) - 1: idx = np.where( (spd_data >= spd_bins[j]) & (spd_data < spd_bins[j + 1]) & (dir_data >= bins[i]) & (dir_data < bins[i + 1]) )[0] else: idx = np.where( (spd_data >= spd_bins[j]) & (dir_data >= bins[i]) & (dir_data < bins[i + 1]) )[0] mean_data[i, j] = np.nanmean(data[idx]) # Necessary to produce the full polar contour without having gaps mean_data = np.insert(mean_data, -1, mean_data[0, :], axis=0) bins.append(bins[0]) mean_data[-1, :] = mean_data[0, :] # In order to properly handle vmin/vmax in contours, need to adjust # the levels plotted and remove the keywords to contourf vmin = np.nanmin(mean_data) vmax = np.nanmax(mean_data) if 'vmin' in kwargs: vmin = kwargs.get('vmin') kwargs.pop('vmin', None) if 'vmax' in kwargs: vmax = kwargs.get('vmax') kwargs.pop('vmax', None) clevels = np.linspace(vmin, vmax, clevels) cplot = self.axes[subplot_index].contourf( np.deg2rad(bins), spd_bins, np.transpose(mean_data), cmap=cmap, levels=clevels, extend='both', **kwargs, ) plot_type_str = 'Mean of' cbar = self.fig.colorbar(cplot, ax=self.axes[subplot_index]) cbar.ax.set_ylabel('Mean') else: raise ValueError('Please choose an available plot type') # Set axis parameters so that it's a standard wind rose style self.axes[subplot_index].set_theta_zero_location('N') self.axes[subplot_index].set_theta_direction(-1) # Set Title sdate = (dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),) edate = (dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[-1]),) if sdate == edate: date_str = 'on ' + sdate[0] else: date_str = 'from ' + sdate[0] + ' to ' + edate[0] if 'units' in ds[data_field].attrs: units = ds[data_field].attrs['units'] else: units = '' if set_title is None: set_title = ' '.join( [plot_type_str, data_field + ' (' + units + ')', 'by\n', dir_field, date_str] ) self.axes[subplot_index].set_title(set_title) plt.tight_layout(h_pad=1.05) return self.axes[subplot_index]