# (c) 2022 DTU Wind Energy
"""
Wind speed time-series plotting including wind speed and wind direction
"""
# Check time series with range slider for subplots
import numpy as np
import pandas as pd
import xarray as xr
from ._helpers import (
HAS_PLOTLY,
check_multipoint,
check_plotting_attrs,
requires_plotly,
)
if HAS_PLOTLY:
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def _missing_data(ds):
"""
Identifies if the data has gaps and returns a new xarray.Dataset or
xarray.DataArray with the mising values added equal to NaN.
"""
t_to_round = ds.time.values + np.timedelta64(500, "ms")
ds = ds.assign_coords({"time": t_to_round.astype(dtype="datetime64[m]")})
if isinstance(ds, xr.Dataset):
if True in np.isnan(ds.to_array().data): # there's already nan values in data
ds_new = ds.expand_dims({"time_2": len(ds.time.values)})
ds_new = ds_new.assign_coords({"time_2": ("time_2", ds.time.values)})
ds_new["wind_speed_2"] = (("time_2"), ds.wind_speed.values)
ds_new["wind_direction_2"] = (("time_2"), ds.wind_direction.values)
ds_new = ds_new.drop_vars("time")
ds_new = ds_new.drop_vars("wind_speed")
ds_new = ds_new.drop_vars("wind_direction")
else:
time_complete = np.array(
pd.date_range(ds.time[0].values, ds.time[-1].values, freq="10min")
)
ws_complete = np.empty(len(time_complete))
wd_complete = np.empty(len(time_complete))
ws_complete[:] = np.nan
wd_complete[:] = np.nan
check = np.isin(time_complete, ds.time)
indxs = np.where(check == True)[0]
np.put(ws_complete, indxs, ds.wind_speed.values)
np.put(wd_complete, indxs, ds.wind_direction.values)
ds_new = ds.expand_dims({"time_2": len(time_complete)})
ds_new = ds_new.assign_coords({"time_2": ("time_2", time_complete)})
ds_new["wind_speed_2"] = (("time_2"), ws_complete)
ds_new["wind_direction_2"] = (("time_2"), wd_complete)
ds_new = ds_new.drop_vars("time")
ds_new = ds_new.drop_vars("wind_speed")
ds_new = ds_new.drop_vars("wind_direction")
if isinstance(ds, xr.DataArray):
if True in np.isnan(ds.data): # there's already nan values in data
data_complete = ds.data.copy()
time_complete = ds.time.values.copy()
ds_new_1 = xr.DataArray(
data=data_complete,
dims=["time_2"],
coords={"time_2": ("time_2", time_complete)},
)
else:
time_complete = np.array(
pd.date_range(ds.time[0].values, ds.time[-1].values, freq="10min")
)
data_complete = np.empty(len(time_complete))
data_complete[:] = np.nan
check = np.isin(time_complete, ds.time)
indxs = np.where(check == True)[0]
np.put(data_complete, indxs, ds.values)
ds_new_1 = xr.DataArray(
data=data_complete,
dims=["time_2"],
coords={"time_2": ("time_2", time_complete)},
)
ds_new = ds_new_1.rename(ds.name + "_2")
return ds_new
def _mark_gaps(da, fig, row, col):
"""
Marks the beggining and ending of regions without data with an hourglass
symbol.
"""
# Need for the indexes where there's change from having to not having data
aux_array = da.data.copy()
aux_array[~np.isnan(aux_array)] = 1 # non nan values to 1
aux_array[np.isnan(aux_array)] = 0 # nan values to 0
# Take indexes where changes happen
change_indexes_1 = np.where(np.roll(aux_array, 1) != aux_array)[0]
change_indexes_2 = np.where(np.roll(aux_array, -1) != aux_array)[0]
change_indexes = np.sort(
np.concatenate(
[
change_indexes_1,
change_indexes_2[~np.isin(change_indexes_2, change_indexes_1)],
]
)
)[1:-1]
# Adding a scatter plot with the marks
fig.add_trace(
go.Scatter(
x=da.time_2.values[change_indexes],
y=da.values[change_indexes],
mode="markers",
hoverinfo="skip",
showlegend=False,
marker=dict(symbol="hourglass", color="red"),
),
row=row,
col=col,
)
return fig
[docs]
def time_series(ds, range_slider=True, time_range=None, mark_data_gaps=False):
"""Create time series plot
The time series plot can be displayed for both a xarray.Dataset o
xarray.DataAarray input argument. When the input is a Dataset, plots
both the wind direction and wind speed time series. If the input is a
DataAarray plot the its data variable. For both cases the gaps of data are
identified in the time series.
Parameters
----------
ds : either a xarray.Dataset or a xarray.DataArray
xarray.Dataset representing the wind direction and wind speed time
series.
xarray.DataArray representing a data variable time series.
range_slider : bool, optional
Include range slider?
Default: with range slider
time_range : list of two values [start, end] that can convert to numpy.datetime64
The time series is directly shown in the defined interval.
e.g: ``time_range=['2015-12-27', '2016-01-12']`` will display the
timeseries for this time interval.
NOTE: all data is still plotted, only the initial view to the data is changed.
Default shows the full data range
mark_data_gaps : bool, optional
Mark beggining and ending of regions with data gaps in the time series?
NOTE: Mark data gaps increases a bit the plotting time (1 to 3 sec).
Default: without marks
Returns
-------
plotly.graph_objects.Figure
Plotly figure for display, additional modification, or output
"""
requires_plotly()
check_multipoint(ds)
dss = ds.squeeze()
dsss = _missing_data(dss)
dff = dsss.to_dataframe().reset_index()
if isinstance(ds, xr.Dataset): # here we plot 2 subplots with wd and ws
fig_subplots = make_subplots(
shared_xaxes=True,
rows=2,
cols=1,
)
xaxes_title = check_plotting_attrs(dss["time"])
yaxes_title_wd = check_plotting_attrs(dss["wind_direction"])
yaxes_title_ws = check_plotting_attrs(dss["wind_speed"])
fig_ws = px.line(
dff,
x="time_2",
y="wind_speed_2",
custom_data=["wind_direction_2", "wind_speed_2"],
)
fig_wd = px.line(
dff,
x="time_2",
y="wind_direction_2",
custom_data=["wind_direction_2", "wind_speed_2"],
)
hovertemplate = (
yaxes_title_ws
+ ": %{customdata[1]:.2f}<br>"
+ yaxes_title_wd
+ ": %{customdata[0]:.2f}"
)
for traces in fig_ws["data"]:
fig_subplots.add_trace(traces, row=1, col=1)
for traces in fig_wd["data"]:
fig_subplots.add_trace(traces, row=2, col=1)
fig_subplots.update_yaxes(title_text=yaxes_title_ws, row=1, col=1)
fig_subplots.update_yaxes(title_text=yaxes_title_wd, row=2, col=1)
fig_subplots.update_xaxes(
title_text=xaxes_title,
rangeslider={"visible": range_slider, "thickness": 0.05},
type="date",
row=2,
col=1,
)
fig_subplots.update_traces(
hovertemplate=hovertemplate, connectgaps=False, row=1, col=1
)
fig_subplots.update_traces(
hovertemplate=hovertemplate, connectgaps=False, row=2, col=1
)
fig = fig_subplots.update_layout(hovermode="x unified", xaxis_range=time_range)
fig.update_traces(connectgaps=False)
if mark_data_gaps:
fig = _mark_gaps(dsss["wind_direction_2"], fig, row=2, col=1)
fig = _mark_gaps(dsss["wind_speed_2"], fig, row=1, col=1)
elif isinstance(ds, xr.DataArray): # here we plot 1 plot with the dataArray
xaxes_title = check_plotting_attrs(dss["time"])
yaxes_title = check_plotting_attrs(dss)
fig = px.line(dff, x="time_2", y=ds.name + "_2", custom_data=[ds.name + "_2"])
hovertemplate = yaxes_title + ": %{customdata[0]:.2f}"
fig.update_yaxes(title_text=yaxes_title)
fig.update_xaxes(
title_text=xaxes_title,
rangeslider={"visible": range_slider, "thickness": 0.05},
type="date",
)
fig.update_traces(hovertemplate=hovertemplate, connectgaps=False)
fig.update_layout(hovermode="x unified", xaxis_range=time_range)
if mark_data_gaps:
fig = _mark_gaps(dsss, fig, row=None, col=None)
return fig