# (c) 2022 DTU Wind Energy
"""
Weibull distribution and histograms plotting
All sector distribution plotting
"""
import numpy as np
from ._helpers import HAS_PLOTLY, has_dash, requires_dash, requires_plotly
if HAS_PLOTLY:
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
if has_dash:
from dash import dcc, html
from dash.dependencies import Input, Output
from jupyter_dash import JupyterDash as Dash
from ..binned_wind_climate import bwc_validate, weibull_fit
from ..weibull import get_weibull_probability
from ..weibull_wind_climate import _is_wwc, wwc_to_bwc
from ._helpers import _get_num_rows_cols, check_multipoint, check_plotting_attrs
from .wind_rose import wind_rose
def _plot_weibull(df_sector, da_sector, sector, uniform_color, weibull, gap):
"""
Plots the weibull distribution and histogram for a sector
"""
# Weibull plot properties
plot_fun_histogram = px.bar
plot_histogram_dict = {
"x": "wsbin",
"y": "wsfreq",
"custom_data": ["wsbin", "wsfreq"],
"title": f"{sector}°",
"color_discrete_sequence": [uniform_color],
}
# Histogram
fig_weibull = plot_fun_histogram(df_sector, **plot_histogram_dict)
yaxes_title = check_plotting_attrs(da_sector)
xaxes_title = check_plotting_attrs(da_sector["wsbin"])
hovertemplate_hist = (
yaxes_title
+ ": %{customdata[1]:.2f}<br>"
+ xaxes_title
+ ": %{customdata[0]:.2f}"
)
fig_weibull.update_yaxes(title_text=yaxes_title)
fig_weibull.update_xaxes(title_text=xaxes_title)
fig_weibull.update_traces(hovertemplate=hovertemplate_hist)
if not gap:
fig_weibull = fig_weibull.update_layout(bargap=0)
# Weibull
if weibull or (weibull is None and "A" in df_sector):
A = float(df_sector.A.sample(1).iloc[0])
k = float(df_sector.k.sample(1).iloc[0])
speeds, prob = get_weibull_probability(
A, k, [np.floor(df_sector.wsbin.min()), np.ceil(df_sector.wsbin.max())]
)
name = f"A = {A:.2f}, k = {k:.2f}"
hovertemplate_weib = yaxes_title + ": %{y:.2f}<br>" + xaxes_title + ": %{x:.2f}"
fig_weibull.add_trace(
go.Scatter(x=speeds, y=prob, name=name, hovertemplate=hovertemplate_weib)
)
fig_weibull = fig_weibull.update_layout(hovermode="closest")
return fig_weibull
def _plot_emergent_curve(ds):
"""
Calculate and plot the emergent probability from a Binned wind climate at
a single point.
"""
speeds_vect = np.linspace(np.floor(ds.wsbin.min()), np.ceil(ds.wsbin.max()), 250)
tot_prob_speed_all_Sect = np.full(
len(speeds_vect),
0.0,
dtype="f",
)
wwc = weibull_fit(ds) # wwc of same spatial extent as the input
ds["A"] = wwc["A"]
ds["k"] = wwc["k"]
for indx, wsi in enumerate(speeds_vect):
for si in range(len(ds["sector"])):
A = ds.isel(sector=si).A
k = ds.isel(sector=si).k
freq = ds.isel(sector=si).wdfreq
speeds, prob_speed_sect_Sect = get_weibull_probability(
A, k, [wsi], single_speed=True
)
tot_prob_speed_all_Sect[indx] = (
tot_prob_speed_all_Sect[indx] + freq * prob_speed_sect_Sect[0]
)
# Ploting
yaxes_title = "Emergent frequency per wind_speed [1]"
xaxes_title = check_plotting_attrs(
ds["wsbin"]
) # Just checking for "wsbin" attrs, not for the calculated "emergent_prob".
hovertemplate_emerg = yaxes_title + ": %{y:.2f}<br>" + xaxes_title + ": %{x:.2f}"
title = "All sectors"
fig_emergent = go.Figure()
fig_emergent.add_trace(
go.Scatter(
x=speeds_vect,
y=tot_prob_speed_all_Sect,
name="Emergent distribution",
hovertemplate=hovertemplate_emerg,
)
)
fig_emergent.update_layout(
title=title,
xaxis_title=xaxes_title,
yaxis_title=yaxes_title,
hovermode="closest",
showlegend=False,
)
return fig_emergent
def _dash_app_layout(df, app):
app.layout = html.Div(
[
html.Div(
[
html.Br(),
html.Div(
"Select or deselect the sectors to be displayed on the wind rose:"
),
html.Br(),
dcc.Dropdown(
id="sector_selection",
options=[{"label": f"{i}", "value": i} for i in df["sector"]],
value=df["sector"],
multi=True,
style={"width": "75%"},
),
]
),
html.Br(),
html.Br(),
html.Div(
[
dcc.Graph(
id="wind_rose",
hoverData={"points": [{"theta": 0}]},
figure={},
)
],
style={
"width": "50%",
"display": "inline-block",
"padding": "0.20",
},
),
html.Div(
[
dcc.Graph(id="weibull_distribution"),
],
style={
"width": "50%",
"display": "inline-block",
"padding": "0.20",
},
),
]
)
return app
[docs]
def histogram_lines(ds, colors="Phase", gap=False):
"""
Create a "distribution" plot and matching frequency wind rose for binned wind climate.
The distribution plot is created by drawing lines across the wind speed bins for each
sector.
Parameters
----------
ds : xarray.Dataset
WindKit Dataset at a single point representing either a binned or weibull wind
climate.
colors : str, optional
str defining a valid plotly built-in color scale (preferred cyclical) name
By default is defined as "Phase".
gap : bool, optional
Include or not gap between sectors
By default is defined no False.
Returns
-------
plotly.graph_objects.Figure
Plotly figure for display, additional modification, or output.
"""
check_multipoint(ds)
requires_plotly()
if _is_wwc(ds): # wwc (wwc) to bwc if wwc (wwc) as input
wwc_pt = ds.squeeze()
ds = wwc_to_bwc(wwc_pt, np.array(range(31)))
if "wsfloor" in ds:
ds = ds.drop_vars("wsfloor")
# Prepare data
ds = ds.squeeze()
dff = ds.to_dataframe().reset_index().dropna()
## Wind rose plot ##
color_scale = [i[1] for i in px.colors.get_colorscale(colors)]
adjusted_color_scale = px.colors.sample_colorscale(color_scale, len(ds.sector) + 1)
fig_rose = wind_rose(ds, uniform_color=adjusted_color_scale)
## Wind distribution plot ##
# Plot propertiees
plot_fun_lines = px.line
plot_lines_dict = {
"x": "wsbin",
"y": "wsfreq",
"custom_data": ["wsbin", "wsfreq", "sector"],
"color": "sector",
}
plot_lines_dict["color"] = [str(i) for i in dff["sector"]]
plot_lines_dict["color_discrete_sequence"] = adjusted_color_scale
fig_distr = plot_fun_lines(dff, **plot_lines_dict)
da_sector = ds.isel(sector=0).wsfreq # All the sectors have same attrs for wsfreq
yaxes_title = check_plotting_attrs(da_sector)
xaxes_title = check_plotting_attrs(da_sector["wsbin"])
hovertemplate = (
"Sector center angle: %{customdata[2]}°<br>"
+ yaxes_title
+ ": %{customdata[1]:.2f}<br>"
+ xaxes_title
+ ": %{customdata[0]:.2f}"
)
# Subplots
fig_subplots = make_subplots(
rows=1, cols=2, specs=[[{"type": "polar"}, {"type": "xy"}]]
)
for traces in fig_rose["data"]:
fig_subplots.add_trace(traces, row=1, col=1)
for traces in fig_distr["data"]:
fig_subplots.add_trace(traces, row=1, col=2)
if not gap:
fig_withoutWeibull = fig_subplots.update_polars(
angularaxis_rotation=90, angularaxis_direction="clockwise", bargap=0
)
else:
fig_withoutWeibull = fig_subplots.update_polars(
angularaxis_rotation=90, angularaxis_direction="clockwise"
)
fig_withoutWeibull.update_yaxes(title_text=yaxes_title, row=1, col=2)
fig_withoutWeibull.update_xaxes(title_text=xaxes_title, row=1, col=2)
fig_withoutWeibull.update_traces(hovertemplate=hovertemplate, row=1, col=2)
fig_withoutWeibull.update_layout(
legend=dict(
title="Sector center angle [°]",
yanchor="top",
y=0.99,
xanchor="right",
x=0.99,
),
hovermode="closest",
)
return fig_withoutWeibull
[docs]
def histogram(
ds,
style="faceted",
color="dodgerblue",
weibull=None,
emergent_distribution=False,
gap=False,
share_yaxes=True,
share_xaxes=True,
):
"""
Plot the histogram represented in a binned wind climate.
Has the option to include the weibull distribution represented as a line over top of
the histogram to qualitatively evaluate the goodness of fit.
Parameters
----------
ds : xarray.Dataset
WindKit Binned wind climate at a single point, optionally with weibull parameters
(A & k) if the weibull fit overlay is desired.
style : str or list of floats, optional
Can take the following values, default is "faceted" (note that "list" is enforced for
single sector datasets, returning the first plot):
- "faceted" : A single plot showing each sector as a separate sub-plot. The
layout of the plot is designed to keep the plot "square".
- "interactive" : Creates a Dash interactive plot that shows both the distribution
and wind rose, hovering over different sectors in the wind rose will show
the corresponding distribution plot
- "list" : Returns a list of plotly figures, one for each sector
color : str, optional
Determines the color used for the histogram bars, default is "dodgerblue".
Strings should define valid CSS-colors.
weibull : bool, optional
Should the weibull plot be drawn, default is None:
- True : Add weibull using A & k from dataset if there, otherwise fit a weibull
and use that for plotting
- False : Don't add weibull to plot
- None : Add weibull if A & k are in dataset
emergent_distribution : bool, optional
Should the emergent distribution be drawn, default is False:
- True : Returns only the emergent distribution plot, ignoring the style
and weibull arguments
- False : Don't plot the emergent distribution
gap : bool, optional
Include a gap between sectors (True), default is False
share_yaxes : bool, optional
Link y-axis values and labels across subplots?, default True
share_xaxes : bool, optional
Link x-axis values and labels across subplots?, default True
Returns
-------
plotly.graph_objects.Figure if style is "faceted" or "emergent_distribution" is True
Plotly figure for display, additional modification, or output
dash.dash.Dash object if style is "interactive"
Dash app (interactive plot) for display, additional modification, or output
List of plotly.graph_objects.Figure if style is "list"
List of Plotly figures for display, additional modification, or output
"""
check_multipoint(ds)
bwc_validate(ds) # Check if the input is actually a bwc, if not breaks
requires_plotly()
if emergent_distribution:
return _plot_emergent_curve(ds)
if weibull and ("A" or "k" not in ds.data_vars):
wwc = weibull_fit(ds) # wwc of same spatial extent as the input
ds["A"] = wwc["A"]
ds["k"] = wwc["k"]
is_single_sector = ds.sizes["sector"] == 1
if not is_single_sector:
ds = ds.squeeze()
# Prepare data
dss = ds.drop_vars("wsfreq")
dss = dss.drop_vars("wsbin")
dss = dss.drop_vars("wsceil")
if "wsfloor" in dss:
dss = dss.drop_vars("wsfloor")
df = dss.to_dataframe().reset_index().dropna()
dff = ds.to_dataframe().reset_index().dropna()
if style == "list" or is_single_sector:
figs = []
for i in range(len(ds.sector)):
df_sector = dff.copy()
df_sector = df_sector[df_sector["sector"] == dss.sector[i].item()]
da_sector = ds.isel(sector=i).wsfreq
fig_weibull = _plot_weibull(
df_sector, da_sector, dss.sector[i].item(), color, weibull, gap
)
fig_weibull.update_layout(
legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99)
)
figs.append(fig_weibull)
return figs if not is_single_sector else figs[0]
elif style == "interactive":
requires_dash()
#### Interactive dash Weibull distribution plots #####
app = Dash(__name__)
# App layout
app = _dash_app_layout(df, app)
# Connect Plotly graphs with Dash Components
@app.callback(
Output(component_id="wind_rose", component_property="figure"),
[Input(component_id="sector_selection", component_property="value")],
)
def update_wind_rose(sectors_selected):
dsss = ds.sel(sector=sectors_selected)
fig_wind_rose = wind_rose(dsss, uniform_color=color, gap=gap)
fig_wind_rose.update_layout(margin={"l": 0, "b": 0, "t": 90, "r": 0})
if gap:
fig_wind_rose.update_polars(bargap=1)
return fig_wind_rose
@app.callback(
Output(component_id="weibull_distribution", component_property="figure"),
[Input(component_id="wind_rose", component_property="hoverData")],
)
def update_weibull_distribution(hoverData):
sector = hoverData["points"][0]["theta"]
df_sector = dff.copy()
df_sector = df_sector[df_sector["sector"] == sector]
indx = np.where(ds.sector.values == sector)
da_sector = ds.isel(sector=indx[0][0]).wsfreq
fig_weibull = _plot_weibull(
df_sector, da_sector, sector, color, weibull, gap
)
fig_weibull.update_layout(
legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99)
)
fig_weibull.update_layout(margin={"l": 0, "b": 0, "t": 90, "r": 0})
return fig_weibull
return app
elif style == "faceted":
if share_yaxes:
share_Yaxes = "all"
else:
share_Yaxes = None
if share_xaxes:
share_Xaxes = "all"
else:
share_Xaxes = None
da_sector = ds.isel(sector=1).wsfreq
yaxes_title = check_plotting_attrs(da_sector)
xaxes_title = check_plotting_attrs(da_sector["wsbin"])
num_plots = len(dss["sector"])
num_rows, num_cols = _get_num_rows_cols(num_plots)
fig_subplots = make_subplots(
rows=num_rows,
cols=num_cols,
subplot_titles=[f"{i}°" for i in dss["sector"].values],
shared_yaxes=share_Yaxes,
shared_xaxes=share_Xaxes,
y_title=yaxes_title,
x_title=xaxes_title,
)
col_num = 0
row_num = 1
for i in range(len(dss["sector"])):
if col_num < num_cols:
col_num += 1
else:
row_num += 1
col_num = 1
df_sector = dff.copy()
df_sector = df_sector[df_sector["sector"] == dss.sector[i].item()]
da_sector = ds.isel(sector=i).wsfreq
fig_weibull = _plot_weibull(
df_sector, da_sector, dss.sector[i].item(), color, weibull, gap
)
for traces in fig_weibull["data"]:
fig_subplots.add_trace(traces, row=row_num, col=col_num)
fig_subplots.update_layout(hovermode="closest", showlegend=True)
if not gap:
fig_subplots = fig_subplots.update_layout(bargap=0)
return fig_subplots