Source code for mhkit.wave.graphics


from mhkit.river.resource import exceedance_probability
from mhkit.river.graphics import _xy_plot
import matplotlib.patheffects as pe
import matplotlib.pyplot as plt
from matplotlib import gridspec
import pandas as pd
import xarray as xr
import numpy as np
import matplotlib
import calendar


[docs]def plot_spectrum(S, ax=None): """ Plots wave amplitude spectrum versus omega Parameters ------------ S: pandas DataFrame Spectral density [m^2/Hz] indexed frequency [Hz] ax : matplotlib axes object Axes for plotting. If None, then a new figure is created. Returns --------- ax : matplotlib pyplot axes """ assert isinstance(S, pd.DataFrame), 'S must be of type pd.DataFrame' f = S.index for key in S.keys(): ax = _xy_plot(f*2*np.pi, S[key]/(2*np.pi), fmt='-', xlabel='omega [rad/s]', ylabel='Spectral density [m$^2$s/rad]', ax=ax) return ax
[docs]def plot_elevation_timeseries(eta, ax=None): """ Plot wave surface elevation time-series Parameters ---------- eta: pandas DataFrame Wave surface elevation [m] indexed by time [datetime or s] ax : matplotlib axes object Axes for plotting. If None, then a new figure is created. Returns ------- ax : matplotlib pyplot axes """ assert isinstance(eta, pd.DataFrame), 'eta must be of type pd.DataFrame' for key in eta.keys(): ax = _xy_plot(eta.index, eta[key], fmt='-', xlabel='Time', ylabel='$\eta$ [m]', ax=ax) return ax
[docs]def plot_matrix( M, xlabel='Te', ylabel='Hm0', zlabel=None, show_values=True, ax=None ): """ Plots values in the matrix as a scatter diagram Parameters ------------ M: pandas DataFrame Matrix with numeric labels for x and y axis, and numeric entries. An example would be the average capture length matrix generated by mhkit.device.wave, or something similar. xlabel: string (optional) Title of the x-axis ylabel: string (optional) Title of the y-axis zlabel: string (optional) Colorbar label show_values : bool (optional) Show values on the scatter diagram ax : matplotlib axes object Axes for plotting. If None, then a new figure is created. Returns --------- ax : matplotlib pyplot axes """ assert isinstance(M, pd.DataFrame), 'M must be of type pd.DataFrame' if ax is None: plt.figure() ax = plt.gca() im = ax.imshow(M, origin='lower', aspect='auto') # Add colorbar cbar = plt.colorbar(im) if zlabel: cbar.set_label(zlabel, rotation=270, labelpad=15) # Set x and y label ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) # Show values in the plot if show_values: for i, col in enumerate(M.columns): for j, index in enumerate(M.index): if not np.isnan(M.loc[index,col]): ax.text(i, j, format(M.loc[index,col], '.2f'), ha="center", va="center") # Reset x and y ticks ax.set_xticks(np.arange(len(M.columns))) ax.set_yticks(np.arange(len(M.index))) ax.set_xticklabels(M.columns) ax.set_yticklabels(M.index) return ax
[docs]def plot_chakrabarti(H, lambda_w, D, ax=None): """ Plots, in the style of Chakrabarti (2005), relative importance of viscous, inertia, and diffraction phemonena Chakrabarti, Subrata. Handbook of Offshore Engineering (2-volume set). Elsevier, 2005. Examples: **Using floats** >>> plt.figure() >>> D = 5 >>> H = 8 >>> lambda_w = 200 >>> wave.graphics.plot_chakrabarti(H, lambda_w, D) **Using numpy array** >>> plt.figure() >>> D = np.linspace(5,15,5) >>> H = 8*np.ones_like(D) >>> lambda_w = 200*np.ones_like(D) >>> wave.graphics.plot_chakrabarti(H, lambda_w, D) **Using pandas DataFrame** >>> plt.figure() >>> D = np.linspace(5,15,5) >>> H = 8*np.ones_like(D) >>> lambda_w = 200*np.ones_like(D) >>> df = pd.DataFrame([H.flatten(),lambda_w.flatten(),D.flatten()], index=['H','lambda_w','D']).transpose() >>> wave.graphics.plot_chakrabarti(df.H, df.lambda_w, df.D) Parameters ---------- H: float or numpy array or pandas Series Wave height [m] lambda_w: float or numpy array or pandas Series Wave length [m] D: float or numpy array or pandas Series Characteristic length [m] ax : matplotlib axes object (optional) Axes for plotting. If None, then a new figure is created. Returns ------- ax : matplotlib pyplot axes """ assert isinstance(H, (np.ndarray, float, int, np.int64,pd.Series)), \ 'H must be a real numeric type' assert isinstance(lambda_w, (np.ndarray, float, int, np.int64,pd.Series)), \ 'lambda_w must be a real numeric type' assert isinstance(D, (np.ndarray, float, int, np.int64,pd.Series)), \ 'D must be a real numeric type' if any([(isinstance(H, np.ndarray) or isinstance(H, pd.Series)), \ (isinstance(lambda_w, np.ndarray) or isinstance(H, pd.Series)), \ (isinstance(D, np.ndarray) or isinstance(H, pd.Series))\ ]): errMsg = 'D, H, and lambda_w must be same shape' n_H = H.squeeze().shape n_lambda_w = lambda_w.squeeze().shape n_D = D.squeeze().shape assert n_H == n_lambda_w and n_H == n_D, errMsg if isinstance(H, np.ndarray): mvals = pd.DataFrame(H.reshape(len(H),1), columns=['H']) mvals['lambda_w'] = lambda_w mvals['D'] = D elif isinstance(H, pd.Series): mvals = pd.DataFrame(H) mvals['lambda_w'] = lambda_w mvals['D'] = D else: H = np.array([H]) lambda_w = np.array([lambda_w]) D = np.array([D]) mvals = pd.DataFrame(H.reshape(len(H),1), columns=['H']) mvals['lambda_w'] = lambda_w mvals['D'] = D if ax is None: plt.figure() ax = plt.gca() ax.set_xscale('log') ax.set_yscale('log') for index, row in mvals.iterrows(): H = row.H D = row.D lambda_w = row.lambda_w KC = H / D Diffraction = np.pi*D / lambda_w label = f'$H$ = {H:g}, $\lambda_w$ = {lambda_w:g}, $D$ = {D:g}' ax.plot(Diffraction, KC, 'o', label=label) if np.any(KC>=10 or KC<=.02) or np.any(Diffraction>=50) or \ np.any(lambda_w >= 1000) : ax.autoscale(enable=True, axis='both', tight=True) else: ax.set_xlim((0.01, 10)) ax.set_ylim((0.01, 50)) graphScale = list(ax.get_xlim()) if graphScale[0] >= .01: graphScale[0] =.01 # deep water breaking limit (H/lambda_w = 0.14) x = np.logspace(1,np.log10(graphScale[0]), 2) y_breaking = 0.14 * np.pi / x ax.plot(x, y_breaking, 'k-') graphScale = list(ax.get_xlim()) ax.text(1, 7, 'wave\nbreaking\n$H/\lambda_w > 0.14$', ha='center', va='center', fontstyle='italic', fontsize='small',clip_on='True') # upper bound of low drag region ldv = 20 y_small_drag = 20*np.ones_like(graphScale) graphScale[1] = 0.14 * np.pi / ldv ax.plot(graphScale, y_small_drag,'k--') ax.text(0.0125, 30, 'drag', ha='center', va='top', fontstyle='italic', fontsize='small',clip_on='True') # upper bound of small drag region sdv = 1.5 y_small_drag = sdv*np.ones_like(graphScale) graphScale[1] = 0.14 * np.pi / sdv ax.plot(graphScale, y_small_drag,'k--') ax.text(0.02, 7, 'inertia \n& drag', ha='center', va='center', fontstyle='italic', fontsize='small',clip_on='True') # upper bound of negligible drag region ndv = 0.25 graphScale[1] = 0.14 * np.pi / ndv y_small_drag = ndv*np.ones_like(graphScale) ax.plot(graphScale, y_small_drag,'k--') ax.text(8e-2, 0.7, 'large\ninertia', ha='center', va='center', fontstyle='italic', fontsize='small',clip_on='True') ax.text(8e-2, 6e-2, 'all\ninertia', ha='center', va='center', fontstyle='italic', fontsize='small', clip_on='True') # left bound of diffraction region drv = 0.5 graphScale = list(ax.get_ylim()) graphScale[1] = 0.14 * np.pi / drv x_diff_reg = drv*np.ones_like(graphScale) ax.plot(x_diff_reg, graphScale, 'k--') ax.text(2, 6e-2, 'diffraction', ha='center', va='center', fontstyle='italic', fontsize='small',clip_on='True') if index > 0: ax.legend(fontsize='xx-small', ncol=2) ax.set_xlabel('Diffraction parameter, $\\frac{\\pi D}{\\lambda_w}$') ax.set_ylabel('KC parameter, $\\frac{H}{D}$') plt.tight_layout()
[docs]def plot_environmental_contour(x1, x2, x1_contour, x2_contour, **kwargs): ''' Plots an overlay of the x1 and x2 variables to the calculate environmental contours. Parameters ---------- x1: numpy array x-axis data x2: numpy array x-axis data x1_contour: numpy array or list Calculated x1 contour values x2_contour: numpy array or list Calculated x2 contour values **kwargs : optional x_label: string (optional) x-axis label. Default None. y_label: string (optional) y-axis label. Default None. data_label: string (optional) Legend label for x1, x2 data (e.g. 'Buoy 46022'). Default None. contour_label: string or list of strings (optional) Legend label for x1_contour, x2_contour countor data (e.g. '100-year contour'). Default None. ax : matplotlib axes object (optional) Axes for plotting. If None, then a new figure is created. Default None. markers: string string or list of strings to use as marker types Returns ------- ax : matplotlib pyplot axes ''' try: x1 = x1.values except: pass try: x2 = x2.values except: pass assert isinstance(x1, np.ndarray), 'x1 must be of type np.ndarray' assert isinstance(x2, np.ndarray), 'x2 must be of type np.ndarray' assert isinstance(x1_contour, (np.ndarray,list)), ('x1_contour must be of ' 'type np.ndarray or list') assert isinstance(x2_contour, (np.ndarray,list)), ('x2_contour must be of ' 'type np.ndarray or list') x_label = kwargs.get("x_label", None) y_label = kwargs.get("y_label", None) data_label=kwargs.get("data_label", None) contour_label=kwargs.get("contour_label", None) ax=kwargs.get("ax", None) markers=kwargs.get("markers", '-') assert isinstance(data_label, (str,type(None))), 'data_label must be of type str' assert isinstance(contour_label, (str,list, type(None))), ('contour_label be of ' 'type str') if isinstance(markers, list): assert all( [isinstance(marker, (str)) for marker in markers] ) elif isinstance(markers, str): markers=[markers] assert all( [isinstance(marker, (str)) for marker in markers] ) else: assert isinstance(markers, (str,list)), ('markers must be of type str or list of strings') assert len(x2_contour) == len(x1_contour), ('contour must be of' f'equal dimesion got {len(x2_contour)} and {len(x1_contour)}') if isinstance(x1_contour, np.ndarray): N_contours=1 x2_contour = [x2_contour] x1_contour = [x1_contour] elif isinstance(x1_contour, list): N_contours=len(x1_contour) if contour_label != None: if isinstance(contour_label, str): contour_label = [contour_label] N_c_labels = len(contour_label) assert N_c_labels == N_contours, ('If specified, the ' 'number of contour lables must be equal to number the ' f'number of contour years. Got {N_c_labels} and {N_contours}') else: contour_label = [None] * N_contours if len(markers)==1: markers=markers*N_contours assert len(markers) == N_contours, ('Markers must be same length' f'as N contours specified. Got: {len(markers)} and {len(x1_contour)}') for i in range(N_contours): contour1 = np.array(x1_contour[i]).T contour2 = np.array(x2_contour[i]).T ax = _xy_plot(contour1, contour2, markers[i], label=contour_label[i], ax=ax) plt.plot(x1, x2, 'bo', alpha=0.1, label=data_label) plt.legend(loc='lower right') plt.xlabel(x_label) plt.ylabel(y_label) plt.tight_layout() return ax
[docs]def plot_avg_annual_energy_matrix( Hm0, Te, J, time_index=None, Hm0_bin_size=None, Te_bin_size=None, Hm0_edges=None, Te_edges=None ): ''' Creates an average annual energy matrix with frequency of occurance. Parameters ---------- Hm0: array-like Significant wave height Te: array-like Energy period J: array-like Energy flux time_index: DateTime Index time to index by. Optional default None. If None Passed parameters must be series indexed by Datetime. Hm0_bin_size: float, int Creates edges of bin using this discrtization. Optional default None. If not passed must pass Hm0_edges. Te_bin_size: float, int Creates edges of bin using this discrtization. Optional default None. If not passed must pass Te_edges. Hm0_edges: array-like Defines the Hm0 bin edges to use. Optional default None. Te_edges: array-like Defines the Te bin edges to use. Optional default None. Returns ------- fig: Figure Average annual energy table plot ''' fig = plt.figure() if isinstance(time_index, type(None)): data = pd.DataFrame(dict(Hm0=Hm0, Te=Te, J=J)) else: data= pd.DataFrame(dict(Hm0=Hm0, Te=Te, J=J), index=time_index) years=data.index.year.unique() if isinstance(Hm0_edges, type(None)): Hm0_max = data.Hm0.max() Hm0_edges = np.arange(0,Hm0_max+Hm0_bin_size,Hm0_bin_size) if isinstance(Te_edges, type(None)): Te_max = data.Te.max() Te_edges = np.arange(0, Te_max+Te_bin_size,Te_bin_size) # Dict for number of hours each sea state occurs hist_counts={} hist_J={} # Create hist of counts, and weghted by J for each year for year in years: year_data = data.loc[str(year)].copy(deep=True) # Get the counts of each bin counts, xedges, yedges= np.histogram2d( year_data.Te, year_data.Hm0, bins = (Te_edges,Hm0_edges), ) # Get centers for number of counts plot location xcenters = xedges[:-1]+ np.diff(xedges) ycenters = yedges[:-1]+ np.diff(yedges) year_data['xbins'] = np.digitize(year_data.Te, xcenters) year_data['ybins'] = np.digitize(year_data.Hm0, ycenters) total_year_J = year_data.J.sum() H=counts.copy() for i in range(len(xcenters)): for j in range(len(ycenters)): bin_J = year_data[(year_data.xbins == i) & (year_data.ybins == j)].J.sum() H[i][j] = bin_J / total_year_J # Save in results dict hist_counts[year] = counts hist_J[year] = H # Calculate avg annual avg_annual_counts_hist = sum(hist_counts.values())/len(years) avg_annual_J_hist = sum(hist_J.values())/len(years) # Create a mask of non-zero weights to hide from imshow Hmasked = np.ma.masked_where(~(avg_annual_J_hist>0),avg_annual_J_hist) plt.imshow(Hmasked.T, interpolation = 'none', vmin = 0.005, origin='lower', aspect='auto', extent=[xedges[0], xedges[-1], yedges[0], yedges[-1]]) # Plot number of counts as text on the hist of annual avg J for xi in range(len(xcenters)): for yi in range(len(ycenters)): if avg_annual_counts_hist[xi][yi] != 0: plt.text( xedges[xi], yedges[yi], int(np.ceil(avg_annual_counts_hist[xi][yi])), fontsize=10, color='white', path_effects=[pe.withStroke(linewidth=1, foreground="k")] ) plt.xlabel('Wave Energy Period (s)') plt.ylabel('Significant Wave Height (m)') cbar=plt.colorbar() cbar.set_label('Mean Normalized Annual Energy') plt.tight_layout() return fig
[docs]def monthly_cumulative_distribution(J): ''' Creates a cumulative distribution of energy flux as described in IEC TS 62600-101. Parameters ---------- J: Series Energy Flux with DateTime index Returns ------- ax: axes Figure of monthly cumulative distribution ''' assert isinstance(J, pd.Series), 'J must be of type pd.Series' cumSum={} months=J.index.month.unique() for month in months: F = exceedance_probability(J[J.index.month==month]) cumSum[month] = 1-F/100 cumSum[month].sort_values('F', inplace=True) plt.figure(figsize=(12,8) ) for month in months: plt.semilogx(J.loc[cumSum[month].index], cumSum[month].F, '--', label=calendar.month_abbr[month]) F = exceedance_probability(J) F.sort_values('F', inplace=True) ax = plt.semilogx(J.loc[F.index], 1-F['F']/100, 'k-', fillstyle='none', label='All') plt.grid() plt.xlabel('Energy Flux') plt.ylabel('Cumulative Distribution') plt.legend() return ax
[docs]def plot_compendium(Hs, Tp, Dp, buoy_title=None, ax=None): """ Create subplots showing: Significant Wave Height (Hs), Peak Period (Tp), and Direction (Dp) using OPeNDAP service from CDIP THREDDS Server. See http://cdip.ucsd.edu/themes/cdip?pb=1&bl=cdip?pb=1&d2=p70&u3=s:100:st:1:v:compendium:dt:201204 for example Compendium plot. Developed based on: http://cdip.ucsd.edu/themes/media/docs/documents/html_pages/compendium.html Parameters ---------- Hs: pandas Series significant wave height Tp: pandas Series significant wave height Dp: pandas Series significant wave height buoy_title: string (optional) Buoy title from the CDIP THREDDS Server ax : matplotlib axes object (optional) Axes for plotting. If None, then a new figure is created. Returns ------- ax : matplotlib pyplot axes """ assert isinstance(Hs, pd.Series), 'Hs must be of type pd.Series' assert isinstance(Tp, pd.Series), 'Tp must be of type pd.Series' assert isinstance(Dp, pd.Series), 'Dp must be of type pd.Series' assert isinstance(buoy_title, (str, type(None))), 'buoy_title must be of type string' f, (pHs, pTp, pDp) = plt.subplots(3, 1, sharex=True, figsize=(15,10)) pHs.plot(Hs.index,Hs,'b') pTp.plot(Tp.index,Tp,'b') pDp.scatter(Dp.index,Dp,color='blue',s=5) pHs.tick_params(axis='x', which='major', labelsize=12, top='off') pHs.set_ylim(0,8) pHs.tick_params(axis='y', which='major', labelsize=12, right='off') pHs.set_ylabel('Hs [m]', fontsize=18) pHs.grid(color='b', linestyle='--') pHs2 = pHs.twinx() pHs2.set_ylim(0,25) pHs2.set_ylabel('Hs [ft]', fontsize=18) # Peak Period, Tp pTp.set_ylim(0,28) pTp.set_ylabel('Tp [s]', fontsize=18) pTp.grid(color='b', linestyle='--') # Direction, Dp pDp.set_ylim(0,360) pDp.set_ylabel('Dp [deg]', fontsize=18) pDp.grid(color='b', linestyle='--') pDp.set_xlabel('Day', fontsize=18) # Set x-axis tick interval to every 5 days degrees = 70 days = matplotlib.dates.DayLocator(interval=5) daysFmt = matplotlib.dates.DateFormatter('%Y-%m-%d') plt.gca().xaxis.set_major_locator(days) plt.gca().xaxis.set_major_formatter(daysFmt) plt.setp( pDp.xaxis.get_majorticklabels(), rotation=degrees ) # Set Titles month_name_start = Hs.index.month_name()[0][:3] year_start = Hs.index.year[0] month_name_end = Hs.index.month_name()[-1][:3] year_end = Hs.index.year[-1] plt.suptitle(buoy_title, fontsize=30) plt.title(f'{Hs.index[0].date()} to {Hs.index[-1].date()}', fontsize=20) ax = f return ax
[docs]def plot_boxplot(Hs, buoy_title=None): """ Create plot of monthly-averaged boxes of Significant Wave Height (Hs) data. Developed based on: http://cdip.ucsd.edu/themes/media/docs/documents/html_pages/annualHs_plot.html Parameters ------------ data: pandas DataFrame Spectral density [m^2/Hz] indexed frequency [Hz] buoy_title: string (optional) Buoy title from the CDIP THREDDS Server ax : matplotlib axes object (optional) Axes for plotting. If None, then a new figure is created. Returns --------- ax : matplotlib pyplot axes """ assert isinstance(Hs, pd.Series), 'Hs must be of type pd.Series' assert isinstance(buoy_title, (str, type(None))), 'buoy_title must be of type string' months = Hs.index.month means = Hs.groupby(months).mean() monthlengths = Hs.groupby(months).count() fig = plt.figure(figsize=(10,12)) gs = gridspec.GridSpec(2,1, height_ratios=[4,1]) boxprops = dict(color='k') whiskerprops = dict(linestyle='--', color='k') flierprops = dict(marker='+', color='r',markeredgecolor='r',markerfacecolor='r') medianprops = dict(linewidth=2.5,color='firebrick') meanprops = dict(linewidth=2.5, marker='_', markersize=25) bp = plt.subplot(gs[0,:]) Hs_months = Hs.to_frame().groupby(months) bp = Hs_months.boxplot(subplots=False, boxprops=boxprops, whiskerprops=whiskerprops, flierprops=flierprops, medianprops=medianprops, showmeans=True, meanprops=meanprops) # Add values of monthly means as text for i, mean in enumerate(means): bp.annotate(np.round(mean,2), (means.index[i],mean),fontsize=12, horizontalalignment='center',verticalalignment='bottom', color='g') # Create a second row of x-axis labels for top subplot newax = bp.twiny() newax.tick_params(which='major', direction='in', pad=-18) newax.set_xlim(bp.get_xlim()) newax.xaxis.set_ticks_position('top') newax.xaxis.set_label_position('top') newax.set_xticks(np.arange(1,13,1)) newax.set_xticklabels(monthlengths,fontsize=10) # Sample 'legend' boxplot, to go underneath actual boxplot bp_sample2 = np.random.normal(2.5,0.5,500) bp2 = plt.subplot(gs[1,:]) meanprops = dict(linewidth=2.5, marker='|', markersize=25) bp2_example = bp2.boxplot(bp_sample2,vert=False,flierprops=flierprops, medianprops=medianprops) sample_mean=2.3 bp2.scatter(sample_mean,1,marker="|",color='g',linewidths=1.0,s=200) for line in bp2_example['medians']: xm, ym = line.get_xydata()[0] for line in bp2_example['boxes']: xb, yb = line.get_xydata()[0] for line in bp2_example['whiskers']: xw, yw = line.get_xydata()[0] bp2.annotate("Median",[xm-0.1,ym-0.3*ym],fontsize=10,color='firebrick') bp2.annotate("Mean",[sample_mean-0.1,0.65],fontsize=10,color='g') bp2.annotate("25%ile",[xb-0.05*xb,yb-0.15*yb],fontsize=10) bp2.annotate("75%ile",[xb+0.26*xb,yb-0.15*yb],fontsize=10) bp2.annotate("Outliers",[xw+0.3*xw,yw-0.3*yw],fontsize=10,color='r') if buoy_title: plt.suptitle(buoy_title, fontsize=30, y=0.97) bp.set_title("Significant Wave Height by Month", fontsize=20, y=1.01) bp2.set_title("Sample Boxplot", fontsize=10, y=1.02) # Set axes labels and ticks months_text = [ m[:3] for m in Hs.index.month_name().unique()] bp.set_xticklabels(months_text,fontsize=12) bp.set_ylabel('Significant Wave Height, Hs (m)', fontsize=14) bp.tick_params(axis='y', which='major', labelsize=12, right='off') bp.tick_params(axis='x', which='major', labelsize=12, top='off') # Plot horizontal gridlines onto top subplot bp.grid(axis='x', color='b', linestyle='-', alpha=0.25) # Remove tickmarks from bottom subplot bp2.axes.get_xaxis().set_visible(False) bp2.axes.get_yaxis().set_visible(False) ax = fig return ax
[docs]def plot_directional_spectrum( spectrum, min=None, fill=True, nlevels=11, name="Elevation Variance", units="m^2" ): """ Create a contour polar plot of a directional spectrum. Parameters ------------ spectrum: xarray.DataArray Spectral data indexed frequency [Hz] and wave direction [deg]. min: float (optional) Minimum value to plot. fill: bool Whether to use `contourf` (filled) instead of `contour` (lines). nlevels: int Number of contour levels to plot. name: str Name of the (integral) spectrum variable. units: str Units of the (integral) spectrum variable. Returns --------- ax : matplotlib pyplot axes """ assert isinstance(spectrum, xr.DataArray), 'spectrum must be a DataArray' if min is not None: assert isinstance(min, float), 'min must be a float' assert isinstance(fill, bool), 'fill must be a bool' assert isinstance(nlevels, int), 'nlevels must be an int' assert isinstance(name, str), 'name must be a string' assert isinstance(units, str), 'units must be a string' a,f = np.meshgrid(np.deg2rad(spectrum.direction), spectrum.frequency) _, ax = plt.subplots(subplot_kw=dict(projection='polar')) tmp = np.floor(np.min(spectrum.data)*10)/10 min = tmp if (min is None) else min max = np.ceil(np.max(spectrum.data)*10)/10 levels = np.linspace(min, max, nlevels) if fill: c = ax.contourf(a, f, spectrum, levels=levels) else: c = ax.contour(a, f, spectrum, levels=levels) cbar = plt.colorbar(c) cbar.set_label(f'Spectrum [{units}/Hz/deg]', rotation=270, labelpad=20) ax.set_title(f'{name} Spectrum') ylabels = ax.get_yticklabels() ylabels = [ilabel.get_text() for ilabel in ax.get_yticklabels()] ylabels = [ilabel + "Hz" for ilabel in ylabels] ticks_loc = ax.get_yticks() ax.set_yticks(ticks_loc) ax.set_yticklabels(ylabels) return ax