Source code for pyiem.plot.windrose

"""A WindrosePlot."""

# pylint: disable=not-callable
import math

import matplotlib.colors as mpcolors

# Third Party
import numpy as np
from matplotlib.ticker import FormatStrFormatter
from metpy.units import units

# Local
from ..reference import Z_OVERLAY2
from .layouts import figure
from .util import update_kwargs_apctx

LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
COLORS = ["#012cff", "#00d5f7", "#7cfd7f", "#fde801", "#ff4503", "#7e0100"]
PLOT_CONVENTION_FROM = "from"
PLOT_CONVENTION_TO = "to"


[docs] class WindrosePlot: """A plot that has a single windrose on it.""" @update_kwargs_apctx def __init__(self, **kwargs): """Construct a WindrosePlot. Note that `matplotlib` hard codes a `C` anchor for the axes, so the axes needs to be moved later, if necessary. If you pass a `figsize` that is square, the legend will be placed at the bottom of the image. Everything else will place it in the top left corner. Args: figsize (float, float): Image figure size in inches. rect ([x,y,width,height]): NDC axes rectangle. rmax (float): Maximum radius of the windrose. plot_convention (str): Either `from` (default) or `to`. """ rect = kwargs.pop("rect", None) figsize = kwargs.pop("figsize", [8, 8]) self.plot_convention = kwargs.pop( "plot_convention", PLOT_CONVENTION_FROM, ) self.legend_anchor = "S" if figsize[0] != figsize[1]: if rect is None: rect = [0.1, 0.07, 0.5, 0.79] # slide to the left # Non-square, place legend in top right self.legend_anchor = "NW" if rect is None: rect = [0, 0.12, 1, 0.76] self.rmax = kwargs.pop("rmax", None) self.fig = figure(figsize=figsize, **kwargs) # The vertical is the limiting factor here, since we need room for # the title and legend at the bottom. self.ax = self.fig.add_axes( rect, projection="polar", theta_offset=np.pi / 2.0, theta_direction=-1, ) self.ax.set_xticks(np.arange(0, 2.0 * np.pi - 0.01, 2.0 * np.pi / 8.0)) self.ax.set_xticklabels(LABELS) self.table = None self.calm_percent = None
[docs] def barplot(self, direction, speed, bins, nsector, **kwargs): """Do the bar plotting work. Args: cmap (colormap,optional): Use matplotlib cmap for bars. """ # compute histogram self.calm_percent, dir_centers, self.table = histogram( speed, direction, bins, nsector ) theta = dir_centers.to(units("radian")).m if self.plot_convention == PLOT_CONVENTION_TO: # Invert theta theta -= math.pi base = np.zeros(dir_centers.m.shape[0]) width = (theta[1] - theta[0]) * 0.8 cmap = kwargs.get("cmap") if cmap is None: cmap = mpcolors.ListedColormap(COLORS, "wrplot") norm = mpcolors.BoundaryNorm(np.arange(len(bins.m) + 1), cmap.N) for col in range(self.table.shape[1]): if col < (bins.m.shape[0] - 1): label = f"{bins.m[col]} - {(bins.m[col + 1] - 0.1):.1f}" else: label = f"{bins.m[col]}+" self.ax.bar( theta, self.table[:, col].m, bottom=base, width=width, align="center", label=label, color=cmap(norm(col)), ) base += self.table[:, col].m if self.rmax is not None: self.ax.set_ylim(0, self.rmax) # Place axis label in least congested spot self.ax.set_rlabel_position(dir_centers.m[np.argmin(base)]) # Append a % on the label self.ax.yaxis.set_major_formatter(FormatStrFormatter("%.1f%%")) # Draw Legend anchor = [0.5, -0.10] if self.legend_anchor == "NW": anchor = [-0.09, 0.9] self.ax.legend( bbox_to_anchor=anchor, loc="center", ncol=1 if self.legend_anchor == "NW" else 6, fontsize=10, mode=None, columnspacing=0.8, handletextpad=0.65, # Ugly hack here due to aliasing in pint for mph title=( f"{'' if self.legend_anchor == 'NW' else 'Wind '}Speed " f"[{'mph' if bins.units == units('mph') else bins.units}]" ), )
[docs] def plot_calm(self): """Clear out the center and plot the calm value.""" maxval = np.max(np.sum(self.table, axis=1)).m if self.rmax is not None: maxval = self.rmax # Clear out the center for plotting the calm percentage self.ax.set_rorigin(0 - maxval * 0.2) # Place Calm Percent in the middle self.ax.text( 0.5, 0.5, f"Calm\n{self.calm_percent.m:.1f}%", ha="center", va="center", transform=self.ax.transAxes, )
[docs] def draw_arrows(self): """Place arrows on the border.""" rmin, rmax = self.ax.get_ylim() for x in self.ax.get_xticks(): xy = (x + 0.001, rmax - (rmax - rmin) * 0.12) xytext = (x + 0.001, rmax + (rmax - rmin) * 0.02) if self.plot_convention == PLOT_CONVENTION_TO: xy, xytext = xytext, xy # https://github.com/matplotlib/matplotlib/issues/5344 self.ax.annotate( "", xy=xy, xytext=xytext, arrowprops=dict( facecolor="None", edgecolor="k", alpha=0.8, shrink=0.09, zorder=10, ), ha="center", va="center", zorder=Z_OVERLAY2, annotation_clip=False, )
[docs] def histogram(speed, direction, bins, nsector): """Create the histogram on the given data. Args: speed (pint.Quantity): wind speed with units attached. direction (pint.Quantity): wind direction from North. bins (pint.Quantity): wind thresholds to use for bining. Any value below the first value is considered calm. The last value is extended to infinity to represent the last bin. Returns: calm_percent (float): the percentage of reports below first bin value. dir_centers (list): the center of the direction bins. table (np.ndarray): The <direction>, <speed> histogram in percent. """ # Figure out the partition size angle = 360.0 / float(nsector) # Create bins based on centered around 0 degree angle_slices dir_bins = np.arange(-angle / 2.0, 360 + angle, angle, dtype=float) dir_centers = np.arange(0.0, 360.0, angle, dtype=float) dirvals = direction.to(units("degree")).m speedvals = speed.to(bins.units).m # compute speed bins speed_bins = bins.m.tolist() speed_bins.insert(0, -np.inf) speed_bins.append(np.inf) # Compute! table = np.histogram2d( x=dirvals, y=speedvals, bins=[dir_bins, speed_bins], density=False )[0] # Convert to percentage table = table * 100.0 / table.sum() # Now we clean up some of the assumptions above # The first and last rows should be combined as they both are north table[0, :] = table[0, :] + table[-1, :] # now drop the last row as unused table = table[:-1, :] # now total up the calm percentage, first col calm_percent = np.sum(table[:, 0]) * units("percent") # drop the first column as unused table = table[:, 1:] * units("percent") return calm_percent, dir_centers * units("degree"), table
[docs] @update_kwargs_apctx def plot(direction, speed, **kwargs): """Create a WindrosePlot, add bars and other standard things. Args: direction (pint.Quantity): wind direction from North. speed (pint.Quantity): wind speeds with units attached. bins (pint.Quantity): wind speed bins to produce the histogram for. nsector (int): The number of directional centers to divide the wind rose into. The first sector is centered on north. rmax (float): Hard codes the max radius value for the polar plot. cmap (colormap): Matplotlib colormap to use. plot_convention (str): Either `from` (default) or `to`. Returns: WindrosePlot """ bins = kwargs.pop("bins", None) if bins is None: bins = np.array([2, 5, 10, 20]) * units("mph") nsector = kwargs.pop("nsector", 8) cmap = kwargs.pop("cmap", None) # kwargs gets passed verbatim to `pyiem.plot.layouts.figure` wp = WindrosePlot(**kwargs) wp.barplot( direction, speed, bins, nsector, cmap=cmap, ) wp.plot_calm() wp.draw_arrows() return wp