Note
Click here
to download the full example code
GRIB - Windrose
# (C) Copyright 2017- ECMWF.
#
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
#
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.
#
import numpy as np
import metview as mv
class WindRoseData:
"""Generate windrose data"""
def __init__(
self, sp=None, direction=None, sectors=16, speed_bins=[], percent=True
):
if len(speed_bins) < 2:
raise ValueError(f"speed_bins must have at least 2 elements!")
self.sectors = sectors
self.dir_step = 360.0 / sectors
self.dir_bins = np.linspace(
int(-self.dir_step / 2),
int(360 + self.dir_step / 2),
int(360 / self.dir_step) + 2,
)
self.speed_max = 0
self.speed_bins = speed_bins
self.max_val = 0
self.percent = percent
self.res = None
self._compute(sp, direction)
def _compute(self, speed, direction):
# remove missing values
mask = ~np.isnan(speed)
if np.any(mask):
speed = speed[mask]
direction = direction[mask]
num = len(speed)
self.speed_max = np.max(speed)
if num > 2 and len(speed) == len(direction):
self.res = np.histogram2d(
speed, direction, bins=[self.speed_bins, self.dir_bins], normed=False
)[0]
# unify the north bins
self.res[:, 0] = self.res[:, 0] + self.res[:, -1]
self.res = self.res[:, :-1]
self.dir_bins = self.dir_bins[:-1]
if self.percent:
self.res *= 100.0 / self.res.sum()
self.max_val = np.max(self.res.sum(axis=0))
def build_sectors(
data,
sector_gap=0,
fill_colours=None,
outline_colours=None,
x_scale=1.0,
y_scale=1.0,
):
"""
Define the windrose sector graphical objects.
If outline_colours is None it will have the same values as fill_colours
"""
gr_lst = []
def _init_colours(cols, default_val):
if cols is None:
cols = "red"
if isinstance(cols, str):
cols = [cols] * (len(data.speed_bins) - 1)
elif isinstance(cols, list):
if len(cols) == 0:
cols = ["red"] * (len(data.speed_bins) - 1)
elif len(cols) < len(data.speed_bins) - 1:
cols = list(cols)
for i in range(len(cols), len(data.speed_bins)):
cols.append(cols[-1])
return cols
# init colours
fill_colours = _init_colours(fill_colours, "red")
if outline_colours is None:
outline_colours = fill_colours
else:
outline_colours = _init_colours(outline_colours, "red")
# sector gap in degrees
if sector_gap < 0.5:
sector_gap = 0
elif sector_gap > data.dir_step - 2:
raise ValueError(
f"sector_gap={sector_gap} is too large! Maximum value={data.dir_step-2}"
)
x_cent = 0
y_cent = 0
d_angle = 0.5
for i in range(len(data.dir_bins) - 1):
start_angle = 90 - data.dir_bins[i]
end_angle = 90 - data.dir_bins[i + 1]
if start_angle > end_angle:
start_angle, end_angle = end_angle, start_angle
start_angle += sector_gap / 2.0
end_angle -= sector_gap / 2.0
for j in range(len(data.speed_bins) - 2, -1, -1):
radius = data.res[0 : j + 1, i].sum()
pie_x = [x_cent]
pie_y = [y_cent]
angle = start_angle
while angle <= end_angle:
pie_x.append(x_scale * radius * np.cos(np.radians(angle)) - x_cent)
pie_y.append(y_scale * radius * np.sin(np.radians(angle)) - y_cent)
angle += d_angle
pie_x.append(x_scale * radius * np.cos(np.radians(end_angle)) - x_cent)
pie_y.append(y_scale * radius * np.sin(np.radians(end_angle)) - y_cent)
pie_x.append(x_cent)
pie_y.append(y_cent)
vis = mv.input_visualiser(input_x_values=pie_x, input_y_values=pie_y)
legend = "on" if i == 1 else "off"
graph = mv.mgraph(
legend=legend,
graph_type="area",
graph_line_colour=outline_colours[j],
graph_shade_colour=fill_colours[j],
)
gr_lst.extend([vis, graph])
return gr_lst
def build_view(
incr=2,
max_val=20,
val_label_size=0.35,
dir_label_size=0.4,
colour="black",
percent=True,
):
"""Define the windrose background"""
add_val_labels = True
add_dir_labels = True
# horizontal and vertical axes has to be hidden
h_axis = mv.maxis(
axis_position="left",
axis_line="off",
axis_grid="off",
axis_title="off",
axis_tick="off",
axis_tick_label="off",
)
v_axis = mv.maxis(
axis_position="bottom",
axis_line="off",
axis_grid="off",
axis_title="off",
axis_tick="off",
axis_tick_label="off",
)
max_val = np.ceil(max_val / incr) * incr
# the view is larger than the max val size because we need space for the
# direction labels
# distance of the dir labels from the outmost circle
dir_label_distance = 1.2
# estimated radial size of a dir label
dir_label_space = 1
# the maximum radial size of the coordinate system
view_max_val = max_val + dir_label_distance + dir_label_space
# NOTE: In order to correctly render the windrose chart (we want
# concentric circles instead of ellipses) we have to
# apply a scaling to compensate the difference between the physical
# width and height of the plot. Please note that while the page size is defined
# in % the superpage size is defined in cm! See also subpage size in the view.
# we define A4 landscape
sp_width = 29.7
sp_height = 21
page_top = 0
page_left = 0
page_width = 80
page_height = 100
x_scale = (sp_height / sp_width) * (page_height / page_width)
y_scale = 1.0
# the view
view = mv.cartesianview(
x_automatic="off",
x_min=-view_max_val,
x_max=view_max_val,
y_automatic="off",
y_min=-view_max_val,
y_max=view_max_val,
horizontal_axis=h_axis,
vertical_axis=h_axis,
subpage_frame="off",
subpage_x_position=5,
subpage_y_position=5,
subpage_x_length=90,
subpage_y_length=90,
)
# size is in % of the physical size of the superpage!
page = mv.plot_page(
top=page_top,
bottom=page_top + page_height,
left=page_left,
right=page_left + page_width,
view=view,
)
# size is in cm!
dw = mv.plot_superpage(
layout_size="custom", custom_width=sp_width, custom_height=sp_height, pages=page
)
gr_lst = []
# build the concentric circles
sp = incr
angle_incr = 2 * np.pi / 180.0
while sp <= max_val:
xp = x_scale * sp * np.cos(np.array([i * angle_incr for i in range(1, 182)]))
yp = sp * np.sin(np.array([i * angle_incr for i in range(1, 182)]))
gr = mv.xy_curve(xp, yp, colour, "solid", 1)
gr_lst.append(gr)
sp += incr
# build direction lines
for angle in np.arange(0, 180, 22.5):
angle_1 = np.radians(angle)
angle_2 = np.radians(angle + 180)
gr_lst.append(
mv.xy_curve(
np.array([np.cos(angle_1), np.cos(angle_2)]) * max_val * x_scale,
np.array([np.sin(angle_1), np.sin(angle_2)]) * max_val,
colour,
"solid",
1,
)
)
# build direction labels
if add_dir_labels:
dir_labels = {
"N": 0,
"NNE": 22.5,
"NE": 45,
"ENE": 67.5,
"E": 90,
"ESE": 112.5,
"SE": 135,
"SSE": 157.5,
"S": 180,
"SSW": 202.5,
"SW": 225,
"WSW": 247.5,
"W": 270,
"WNW": 292.5,
"NW": 315,
"NNW": 337.5,
}
r_label = max_val + dir_label_distance
angle_label = 90 - np.array(list(dir_labels.values()))
idx_label = list(range(len(dir_labels)))
vis = mv.input_visualiser(
input_plot_type="xy_point",
input_x_values=x_scale * r_label * np.cos(np.radians(angle_label)),
input_y_values=r_label * np.sin(np.radians(angle_label)),
input_values=idx_label,
)
sym = mv.msymb(
legend="off",
symbol_type="text",
symbol_table_mode="advanced",
symbol_advanced_table_selection_type="list",
symbol_advanced_table_level_list=[*idx_label, idx_label[-1] + 1],
symbol_advanced_table_height_list=0.001,
symbol_advanced_table_text_list=list(dir_labels.keys()),
symbol_advanced_table_text_font_size=dir_label_size,
symbol_advanced_table_text_font_style="normal",
symbol_advanced_table_text_font_colour="black",
symbol_advanced_table_text_display_type="centre",
)
gr_lst.extend([vis, sym])
# build value labels
if add_val_labels:
val_label = [str(i) for i in np.arange(incr, max_val, incr)]
if percent:
val_label = [f"{i}%" for i in val_label]
angle = np.radians(90)
idx_label = list(range(len(val_label)))
vis = mv.input_visualiser(
input_plot_type="xy_point",
input_x_values=np.cos(angle)
* np.array([sp for sp in np.arange(incr, max_val, incr)]),
input_y_values=np.sin(angle)
* np.array([sp for sp in np.arange(incr, max_val, incr)]),
input_values=idx_label,
)
idx_label_level = list(idx_label)
idx_label_level.append(idx_label_level[-1] + 1)
sym = mv.msymb(
legend="off",
symbol_type="text",
symbol_text_blanking="on",
symbol_table_mode="advanced",
symbol_advanced_table_selection_type="list",
symbol_advanced_table_level_list=idx_label_level,
symbol_advanced_table_height_list=0.001,
symbol_advanced_table_text_list=val_label,
symbol_advanced_table_text_font_size=val_label_size,
symbol_advanced_table_text_font_style="normal",
symbol_advanced_table_text_font_colour="black",
symbol_advanced_table_text_display_type="centre",
)
gr_lst.extend([vis, sym])
return dw, gr_lst, x_scale, y_scale
# get data
use_mars = False
if use_mars:
# get data from MARS
f = mv.retrieve(
levelist=850,
param=["u", "v"],
date=20171016,
time=0,
area=[30, -40, 60, 30],
grid=[0.5, 0.5],
)
else:
# read from grib file
filename = "ophelia_wind_850.grib"
if mv.exist(filename):
f = mv.read(filename)
else:
f = mv.gallery.load_dataset(filename)
# get u and v data
u = mv.read(data=f, param="u")
v = mv.read(data=f, param="v")
# compute speed and direction
sp = mv.speed(u, v).values()
direct = mv.direction(u, v).values()
# define speed bins and colours
speed_bins = np.array([0, 5, 10, 15, 20, 25, 30, 100])
speed_colours = [
"RGB(0.9529,0.4392,0.1294)",
"RGB(0.9922,0.6196,0.4353)",
"RGB(0.9961,0.8902,0.4706)",
"RGB(0.5922,0.8392,0.9098)",
"RGB(0.3020,0.7176,0.5922)",
"RGB(0.4000,0.4000,1.0000)",
"RGB(0.3922,0.3255,0.7137)",
]
# generate windrose data
wr = WindRoseData(sp, direct, speed_bins=speed_bins, percent=True)
# generate windrose view
dw, gr_lst, x_scale, y_scale = build_view(incr=2, max_val=wr.max_val, colour="black")
# generate windrose sector objects
gr_wind = build_sectors(
wr,
sector_gap=0,
fill_colours=speed_colours,
outline_colours=None,
x_scale=x_scale,
y_scale=y_scale,
)
# define legend with custom labels. Here we have to follow the order the sectors
# were built, i.e. we need to go from high wind speeds to low wind speeds
legend_text = [
f"{speed_bins[i]} - {speed_bins[i + 1]}" for i in range(len(speed_bins) - 2, -1, -1)
]
legend_text[0] = f"{speed_bins[-2]}+"
legend = mv.mlegend(
legend_title="on",
legend_title_text="Wind speed (m/s)",
legend_title_font_size=0.4,
legend_title_position="top",
legend_text_colour="black",
legend_box_mode="positional",
legend_display_type="disjoint",
legend_text_composition="user_text_only",
legend_user_lines=legend_text,
legend_text_font_size=0.4,
legend_entry_plot_direction="column",
legend_box_x_position=23,
legend_box_y_position=12,
legend_box_x_length=1,
legend_box_y_length=7,
legend_entry_text_width=2,
)
# get metadata for the title
meta = mv.grib_get(u[0], ["level", "date", "time", "step"])[0]
# define title
title = mv.mtext(
text_lines=f"Windrose {meta[0]} hPa Run: {meta[1]} {meta[2]} UTC +{meta[3]}h",
text_font_size=0.5,
)
# define the output plot file
mv.setoutput(mv.pdf_output(output_name="windrose"))
# generate the plot
mv.plot(dw, gr_wind, gr_lst, legend, title)