Source code for ladybug_charts.to_figure

"""Create plotly figures from pandas Dataframe."""

import numpy as np
import pandas as pd

from math import ceil, floor, cos, radians
from typing import Union, List, Tuple
from random import randint
from datetime import timedelta

import plotly.io as pio
import plotly.graph_objects as go
from plotly.graph_objects import Figure
from plotly.graph_objects import Bar
from plotly.subplots import make_subplots

from ._to_dataframe import dataframe, Frequency, MONTHS
from ._helper import discontinuous_to_continuous, rgb_to_hex, ColorSet, color_set,\
    get_monthly_values, group_monthly
from ._helper_chart import get_dummy_trace
from ._psych import _psych_chart
from .utils import Strategy, StrategyParameters

from ladybug.datacollection import HourlyContinuousCollection, \
    HourlyDiscontinuousCollection, MonthlyCollection, DailyCollection, BaseCollection
from ladybug.windrose import WindRose
from ladybug.color import Color, Colorset, ColorRange
from ladybug_pandas.series import Series
from ladybug.sunpath import Sunpath
from ladybug.psychchart import PsychrometricChart
from ladybug.dt import DateTime
from ladybug_comfort.chart.polygonpmv import PolygonPMV
from ladybug.psychrometrics import wet_bulb_from_db_rh
from ladybug.datatype.temperature import WetBulbTemperature
from ladybug.legend import Legend

from ladybug.epw import EPW

# TODO
# To effectively unit test all of these functions need to be re-written with the
# concept of separate code-behind and visualization.

# set white background in all charts
pio.templates.default = 'plotly_white'


[docs] def heat_map( hourly_data: Union[HourlyContinuousCollection, HourlyDiscontinuousCollection], min_range: float = None, max_range: float = None, colors: List[Color] = None, title: str = None, show_title: bool = False, num_labels: int = None, labels: List[float] = None ) -> Figure: """Create a plotly heat map figure from Ladybug Hourly data. Args: hourly_data: A Ladybug HourlyContinuousCollection object or a Ladybug HourlyDiscontinuousCollection object. min_range: The minimum value for the legend of the heatmap. If not set, value will be calculated based on data. Defaults to None. max_range: The maximum value for the legend of the heatmap. If not set, value will be calculated based on data. Defaults to None. colors: A list of Ladybug Color objects. Defaults to None. title: A string to be used as the title of the plot. If not set, the name of the data will be used. Defaults to None. show_title: A boolean to show or hide the title of the chart. Defaults to False. num_labels: The number of labels to be used in the legend. Defaults to None. labels: A list of floats to be used as labels for the legend. Defaults to None. Returns: A plotly figure. """ assert isinstance(hourly_data, (HourlyContinuousCollection, HourlyDiscontinuousCollection)), 'Only Ladybug'\ ' HourlyContinuousCollection and HourlyDiscontinuousCollection are supported.'\ f' Instead got {type(hourly_data)}' if isinstance(hourly_data, HourlyDiscontinuousCollection): hourly_data, data_range = discontinuous_to_continuous(hourly_data) else: data_range = [hourly_data.min, hourly_data.max] var = hourly_data.header.data_type.name df = dataframe() series = Series(hourly_data) df[var] = series.values var_unit = df[var].dtype.name.split('(')[-1].split(')')[0] range_z = [data_range[0], data_range[1]] if min_range is not None: range_z[0] = min_range if max_range is not None: range_z[1] = max_range if not colors: colors = color_set[ColorSet.original.value] nticks = num_labels dtick = labels fig = go.Figure( data=go.Heatmap( y=df["hour"], x=df["UTC_time"].dt.date, z=df[var], zmin=range_z[0], zmax=range_z[1], colorscale=[rgb_to_hex(color) for color in colors], customdata=np.stack((df["month_names"], df["day"]), axis=-1), hovertemplate=( "<b>" + var + ": %{z} " + var_unit + "</b><br>Month: %{customdata[0]}<br>Day: %{customdata[1]}<br>" + "Hour: %{y}:00<br>" ), name="", colorbar=dict(title=var_unit, nticks=nticks, dtick=dtick, thickness=10), ) ) fig.update_xaxes(dtick="M1", tickformat="%b", ticklabelmode="period") fig.update_yaxes(title_text="Hours of the day") # setting the title for the figure if show_title: fig_title = { 'text': title if title else var, 'y': 1, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top', } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_layout( template='plotly_white', margin=dict( l=20, r=20, t=33, b=20), yaxis_nticks=13, title=fig_title, title_pad=dict(t=5) ) fig.update_xaxes(showline=True, linewidth=1, linecolor="black", mirror=True) fig.update_yaxes(showline=True, linewidth=1, linecolor="black", mirror=True) return fig
def _monthly_bar(data: MonthlyCollection, var: str, var_unit: str, color: Color = None) -> Bar: """Create a monthly chart figure data from Ladybug Monthly data. Args: data: A Ladybug MonthlyCollection object. var: A Ladybug variable name. var_unit: A Ladybug variable unit. color: A Ladybug Color object. Defaults to None. Returns: A plotly Bar object. """ df = dataframe(Frequency.MONTHLY) color = color if color else Color( randint(0, 255), randint(0, 255), randint(0, 255)) return go.Bar( x=df['month_names'], y=[round(val, 2) for val in data.values], customdata=np.stack((df["month_names"],), axis=-1), hovertemplate=( '<br>%{y} ' + var_unit + ' ' + var + '<extra></extra>'), marker_color=rgb_to_hex(color), marker_line_color='black', name=var ) def _daily_bar(data: DailyCollection, var: str, var_unit: str, color: Color = None) -> Bar: """Create a daily chart figure data from Ladybug Daily data. Args: data: A Ladybug DailyCollection object. var: A Ladybug variable name. var_unit: A Ladybug variable unit. color: A Ladybug Color object. Defaults to None. Returns: A plotly Bar object. """ df = dataframe(Frequency.DAILY) color = color if color else Color( randint(0, 255), randint(0, 255), randint(0, 255)) return go.Bar( x=df["UTC_time"].dt.date, y=[round(val, 2) for val in data.values], customdata=np.stack((df["month_names"], df["day"]), axis=-1), hovertemplate=( '<br>%{y} ' + var_unit + ' on %{customdata[0]}' + ' %{customdata[1]} <br>' + '<extra></extra>'), marker_color=rgb_to_hex(color), name=var + ' ' + var_unit )
[docs] def bar_chart(data: Union[List[MonthlyCollection], List[DailyCollection]], min_range: float = None, max_range: float = None, colors: List[Color] = None, title: str = None, center_title: bool = False, stack: bool = False) -> Figure: """Create a plotly bar chart figure from multiple ladybug monthly or daily data. Args: data: A list of either ladybug MonthlyCollection data or DailyCollection data. min_range: Minimum value for the legend. If None, it is autocalculated from the data. (Default: None). max_range: Maximum value for the legend. If None, it is autocalculated from the data. (Default: None). colors: A list of ladybug color objects that matches the length of data argument. If None, random colors will be used. (Default: None). title: A string to be used as the title of the plot. (Default: None). center_title: A boolean to set whether to center the title of the chart. (Default: False). stack: A boolean to determine whether to stack the data. (Default: False). Returns: A plotly figure. """ assert len(data) > 0 and all([isinstance(item, (MonthlyCollection, DailyCollection)) for item in data]), 'Only a list of ladybug '\ f' monthly data or ladybug daily data is supported. Instead got {type(data)}' if colors: assert len(colors) == len(data), 'Length of colors argument needs to match'\ f' the length of data argument. Instead got {len(colors)} and {len(data)}' # set the range of y-axis and get the title if provided y_range = None if min_range is None or max_range is None else [min_range, max_range] y_title = '{} ({})'.format(data[0].header.data_type, data[0].header.unit) fig = go.Figure() for count, item in enumerate(data): var = item.header.metadata['type'] if 'type' in \ item.header.metadata else str(item.header.data_type) var_unit = item.header.unit color = colors[count] if colors else None bar = _monthly_bar(item, var, var_unit, color) \ if isinstance(item, MonthlyCollection) \ else _daily_bar(item, var, var_unit, color) fig.add_trace(bar) # set the title for the figure fig_title = None if title is not None: fig_title = { 'text': title, 'y': 1, 'x': 0, 'yanchor': 'top' } if center_title: fig_title['x'] = 0.5 fig_title['xanchor'] = 'center' # move legend upwards as mode data is loaded leg = {'x': 0, 'y': 1.2} if len(data) <= 3 else {} fig.update_layout( barmode='relative' if stack else 'group', template='plotly_white', plot_bgcolor='white', margin=dict(l=20, r=20, t=33, b=20), yaxis_nticks=13, title=fig_title, legend=leg ) fig.update_xaxes(dtick="M1", tickformat="%b", ticklabelmode="period", showline=True, linewidth=1, linecolor="black", mirror=True) fig.update_yaxes(showline=True, linewidth=1, linecolor="black", mirror=True, range=y_range, title_text=y_title) return fig
def _bar_chart_single_data(data: Union[MonthlyCollection, DailyCollection], chart_type: str = 'monthly', title: str = None, show_title: bool = False, color: Color = None) -> Figure: """Create a plotly bar chart figure from a ladybug monthly or daily data object. Args: data: A ladybug monthly or daily data object. chart_type: A string to determine the type of chart to be created. Accepted values are 'monthly' and 'daily'. Defaults to 'monthly'. title: A string to be used as the title of the plot. If not set, the names of data will be used to create a title for the chart. Defaults to None. show_title: A boolean to set whether to show the title of the chart. Defaults to False. color: A ladybug color object. If not set, random colors will be used. Returns: A plotly figure. """ if chart_type == 'monthly': var = data.header.data_type.name var_unit = data.header.unit bar = _monthly_bar(data, var, var_unit, color) else: var = data.header.data_type.name var_unit = data.header.unit bar = _daily_bar(data, var, var_unit, color) chart_title = title if title else var fig = go.Figure(bar) fig.update_xaxes(dtick="M1", tickformat="%b", ticklabelmode="period") fig.update_yaxes(title_text='('+var_unit+')') # setting the title for the figure if show_title: fig_title = { 'text': chart_title, 'y': 1, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_layout( template='plotly_white', margin=dict(l=20, r=20, t=33, b=20), yaxis_nticks=13, title=fig_title, ) fig.update_xaxes(showline=True, linewidth=1, linecolor="black", mirror=True) fig.update_yaxes(showline=True, linewidth=1, linecolor="black", mirror=True) return fig
[docs] def monthly_bar_chart(data: MonthlyCollection, title: str = None, show_title: bool = False, color: Color = None) -> Figure: """Create a plotly bar chart figure from a ladybug monthly data object. Args: data: A ladybug MonthlyCollection object. title: A string to be used as the title of the plot. If not set, the name of the data will be used. Defaults to None. show_title: A boolean to set whether to show the title of the chart. Defaults to False. color: A Ladybug color object. If not set, a random color will be used. Defaults to None. Returns: A plotly figure. """ assert isinstance(data, MonthlyCollection), 'Only ladybug monthly data is'\ f' supported. Instead got {type(data)}' return _bar_chart_single_data(data, 'monthly', title, show_title, color=color)
[docs] def daily_bar_chart(data: DailyCollection, title: str = None, show_title: bool = False, color: Color = None) -> Figure: """Create a plotly bar chart figure from a ladybug daily data object. Args: data: A ladybug DailyCollection object. title: A string to be used as the title of the plot. If not set, the name of the data will be used. Defaults to None. show_title: A boolean to determine whether to show the title of the plot. Defaults to False. color: A Ladybug color object. If not set, a random color will be used. Defaults to None. Returns: A plotly figure. """ assert isinstance(data, DailyCollection), 'Only ladybug daily data is'\ f' supported. Instead got {type(data)}' return _bar_chart_single_data(data, 'daily', title, show_title, color)
[docs] def hourly_line_chart(data: HourlyContinuousCollection, color: Color = None, title: str = None, show_title: bool = False) -> Figure: """Create a plotly line chart figure from a ladybug hourly continuous data object. Args: data: A ladybug HourlyContinuousCollection object. color: A Ladybug color object. If not set, a random color will be used. Defaults to None. title: A string to be used as the title of the plot. Defaults to None. show_title: A boolean to determine whether to show the title of the plot. Defaults to False. Returns: A plotly figure. """ assert isinstance(data, HourlyContinuousCollection), \ f'Only ladybug hourly continuous data is supported. Instead got {type(data)}' var = data.header.data_type.name var_unit = data.header.unit var_color = color if color else Color( randint(0, 255), randint(0, 255), randint(0, 255)) df = dataframe() series = Series(data) df[var] = series.values df[var] = df[var].astype(float) data_max = 5 * ceil(df[var].max() / 5) data_min = 5 * floor(df[var].min() / 5) range_y = [data_min, data_max] # Get min, max, and mean of each day dbt_day = df.groupby(np.arange(len(df.index)) // 24)[var].agg( ["min", "max", "mean"] ) trace1 = go.Bar( x=df["UTC_time"].dt.date.unique(), y=dbt_day["max"] - dbt_day["min"], base=dbt_day["min"], marker_color=rgb_to_hex(var_color), marker_opacity=0.3, name=var + " Range", customdata=np.stack( (dbt_day["mean"], df.iloc[::24, :]["month_names"], df.iloc[::24, :]["day"]), axis=-1, ), hovertemplate=( "Max: %{y:.2f} " + var_unit + "<br>Min: %{base:.2f} " + var_unit + "<br><b>Ave : %{customdata[0]:.2f} " + var_unit + "</b><br>Month: %{customdata[1]}<br>Day: %{customdata[2]}<br>" + "<extra></extra>" ), ) trace2 = go.Scatter( x=df["UTC_time"].dt.date.unique(), y=dbt_day["mean"], name="Average " + var, mode="lines", marker_color=rgb_to_hex(var_color), marker_opacity=1, customdata=np.stack( (dbt_day["mean"], df.iloc[::24, :]["month_names"], df.iloc[::24, :]["day"]), axis=-1, ), hovertemplate=( "<b>Ave : %{customdata[0]:.2f} " + var_unit + "</b><br>Month: %{customdata[1]}<br>Day: %{customdata[2]}<br>" + "<extra></extra>" ), ) data = [trace1, trace2] fig = go.Figure( data=data, layout=go.Layout(barmode="overlay", bargap=0, margin=dict( l=20, r=20, t=33, b=20)) ) # setting the title for the figure if show_title: fig_title = { 'text': title if title else var, 'y': 1, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_xaxes( dtick="M1", tickformat="%b", ticklabelmode="period", showline=True, linewidth=1, linecolor="black", mirror=True, ) fig.update_yaxes( range=range_y, title_text=f'({var_unit})', showline=True, linewidth=1, linecolor="black", mirror=True, ) fig.update_layout( legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), template='plotly_white', title=fig_title ) return fig
[docs] def diurnal_average_chart_from_hourly( data: HourlyContinuousCollection, title: str = None, show_title: bool = False, color: Color = None ) -> Figure: """Create a diurnal average chart from a ladybug hourly continuous data. Args: data: A ladybug HourlyContinuousCollection object. title: A string to be used as the title of the plot. Defaults to None. show_title: A boolean to determine whether to show the title of the plot. Defaults to False. color: A Ladybug color object. If not set, a random color will be used. Defaults to None. Returns: A plotly figure. """ assert isinstance(data, HourlyContinuousCollection), \ f'Only ladybug hourly continuous data is supported. Instead got {type(data)}' # get monthly per hour average data monthly_values = get_monthly_values(data.average_monthly_per_hour()) monthly_lower_values = get_monthly_values(data.percentile_monthly_per_hour(0)) monthly_higher_values = get_monthly_values(data.percentile_monthly_per_hour(100)) var = data.header.data_type.name var_unit = data.header.unit var_color = color if color else Color( randint(0, 255), randint(0, 255), randint(0, 255)) var_color = rgb_to_hex(var_color) fig = go.Figure() for i in range(12): x = [[MONTHS[i]]*24, list(range(0, 24))] # add lower range fig.add_trace( go.Scatter( x=x, y=monthly_lower_values[i], line_color=var_color, line_width=0, opacity=0.2, showlegend=False, hovertemplate=( "<b>" + var+' low' + ": %{y:.2f} " + var_unit + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" ), ) ) # add higher range fig.add_trace( go.Scatter( x=x, y=monthly_higher_values[i], line_color=var_color, fill='tonexty', line_width=0, opacity=0.1, showlegend=False, hovertemplate=( "<b>" + var + ' high' + ": %{y:.2f} " + var_unit + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" ),) ) # add MonthlyPerHour average dry-bulb temperature fig.add_trace( go.Scatter( x=x, y=monthly_values[i], line_color=var_color, line_width=2, showlegend=False, hovertemplate=( "<b>" + var + ": %{y:.2f} " + var_unit + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" )) ) # setting the title for the figure if show_title: fig_title = { 'text': title if title else var + f' ({var_unit})', 'y': 0.85, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_layout( xaxis=dict( showdividers=False, showline=True, linecolor='black', linewidth=1, ticks='outside', tickson='boundaries', tickwidth=1, ticklen=5), yaxis=dict( showline=True, linecolor='black', linewidth=1, title=var_unit), title=fig_title, ) return fig
[docs] def diurnal_average_chart( epw: EPW, title: str = None, show_title: bool = False, colors: Union[List[Color], Tuple[Color]] = Colorset.original() ) -> Figure: """Create a diurnal average chart from a ladybug EPW object. Args: epw: A ladybug EPW object. title: A string to be used as the title of the plot. Defaults to None. show_title: A boolean to determine whether to show the title of the plot. Defaults to False. colorset: A ColorSet object. Defaults to ColorSet.original. Returns: A plotly figure. """ # reset colors if length of colors is less than 5 if len(colors) < 5: color_range = ColorRange(colors, domain=[0, 5]) num_of_colors: int = len(colors)*2 if len(colors)*2 >= 5 else 5 colors: List[Color] = [color_range.color(i) for i in range(num_of_colors)] dbt_color = rgb_to_hex(colors[-1]) wbt_color = rgb_to_hex(colors[-2]) glob_hor_rad_color = rgb_to_hex(colors[-3]) dir_nor_rad_color = rgb_to_hex(colors[-4]) diff_hor_rad_color = rgb_to_hex(colors[-5]) spread_color = rgb_to_hex(Color(149, 152, 156)) # get monthly per hour average data glob_hor_rad = get_monthly_values( epw.global_horizontal_radiation.average_monthly_per_hour()) dir_nor_rad = get_monthly_values( epw.direct_normal_radiation.average_monthly_per_hour()) diff_hor_rad = get_monthly_values( epw.diffuse_horizontal_radiation.average_monthly_per_hour()) dry_bulb_temp = get_monthly_values( epw.dry_bulb_temperature.average_monthly_per_hour()) dry_bulb_temp_low = get_monthly_values( epw.dry_bulb_temperature.percentile_monthly_per_hour(0)) dry_bulb_temp_high = get_monthly_values( epw.dry_bulb_temperature.percentile_monthly_per_hour(100)) wet_bulb = HourlyContinuousCollection.compute_function_aligned( wet_bulb_from_db_rh, [epw.dry_bulb_temperature, epw.relative_humidity, epw.atmospheric_station_pressure], WetBulbTemperature(), 'C') wet_bulb_temp = get_monthly_values(wet_bulb.average_monthly_per_hour()) fig = go.Figure() for i in range(12): x = [[MONTHS[i]]*24, list(range(0, 24))] # add lower dry-bulb temperature fig.add_trace( go.Scatter( x=x, y=dry_bulb_temp_low[i], line_color=spread_color, line_width=0, opacity=0.2, yaxis='y2', showlegend=False, hovertemplate=( "<b>" + 'Dry-bulb temperature low' + ": %{y:.2f} " + 'C' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" ), ) ) # add higher dry-bulb temperature fig.add_trace( go.Scatter( x=x, y=dry_bulb_temp_high[i], line_color=spread_color, fill='tonexty', line_width=0, opacity=0.1, yaxis='y2', showlegend=False, hovertemplate=( "<b>" + 'Dry-bulb temperature high' + ": %{y:.2f} " + 'C' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" ),) ) # add global horizontal radiation fig.add_trace( go.Scatter( x=x, y=glob_hor_rad[i], fill='tozeroy', line_width=1, line_color=glob_hor_rad_color, yaxis='y', showlegend=False, hovertemplate=( "<b>" + 'Monthly per hour average Global horizontal radiation' + ": %{y:.2f} " + 'Wh/m2' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" )) ) # add direct normal radiation fig.add_trace( go.Scatter( x=x, y=dir_nor_rad[i], fill='tozeroy', line_width=1, line_color=dir_nor_rad_color, yaxis='y', showlegend=False, hovertemplate=( "<b>" + 'Monthly per hour average Direct normal radiation' + ": %{y:.2f} " + 'Wh/m2' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" )) ) # add diffuse horizontal radiation fig.add_trace( go.Scatter( x=x, y=diff_hor_rad[i], fill='tozeroy', line_width=1, line_color=diff_hor_rad_color, yaxis='y', showlegend=False, hovertemplate=( "<b>" + 'Monthly per hour average Diffused horizontal radiation' + ": %{y:.2f} " + 'Wh/m2' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" )) ) # add MonthlyPerHour average dry-bulb temperature fig.add_trace( go.Scatter( x=x, y=dry_bulb_temp[i], line_color=dbt_color, line_width=2, yaxis='y2', showlegend=False, hovertemplate=( "<b>" + 'Monthly per hour average dry-bulb temperature' + ": %{y:.2f} " + 'C' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" )) ) # add MonthlyPerHour average wet-bulb temperature fig.add_trace( go.Scatter( x=x, y=wet_bulb_temp[i], line_color=wbt_color, line_width=2, yaxis='y2', showlegend=False, hovertemplate=( "<b>" + 'Monthly per hour average wet-bulb temperature' + ": %{y:.2f} " + 'C' + "</b><br>Month: %{x[0]}<br>Hour: %{x[1]}:00<br>" + "<extra></extra>" )) ) # Add dummy traces to create legend fig.add_trace(get_dummy_trace( 'Diffused horizontal radiation', diff_hor_rad_color)) fig.add_trace(get_dummy_trace( 'Direct normal radiation', dir_nor_rad_color)) fig.add_trace(get_dummy_trace( 'Global horizontal radiation', glob_hor_rad_color)) fig.add_trace(get_dummy_trace( 'Wet-bulb temperature', wbt_color)) fig.add_trace(get_dummy_trace( 'Dry-bulb temperature', dbt_color)) # setting the title for the figure if show_title: fig_title = { 'text': title if title else 'Diurnal Average', 'y': 0.95, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_layout( xaxis=dict( showdividers=False, showline=True, linecolor='black', linewidth=1, ticks='outside', tickson='boundaries', tickwidth=1, ticklen=5), yaxis=dict( range=[0, 1600], tick0=0, dtick=100, title='Radiation Wh/m2', showline=True, linecolor='black', linewidth=1), yaxis2=dict( range=[-20, 60], tick0=-20, dtick=5, title='Temperature C', overlaying='y', side='right', showline=True, linecolor='black', linewidth=1), title=fig_title, legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 ) ) return fig
def _analysis_data_labels(bins, units): """Return labels for a wind speed range.""" labels = [] for left, right in zip(bins[:-1], bins[1:]): labels.append("{} - {} {}".format(left, right, units)) return labels
[docs] def wind_rose(lb_wind_rose: WindRose, title: str = None, show_title: bool = False) -> Figure: """Create a windrose plot. Args: lb_wind_rose: A ladybug WindRose object. title: A title for the plot. Defaults to None. show_title: A boolean to show or hide the title. Defaults to False. Returns: A plotly figure. """ assert isinstance(lb_wind_rose, WindRose), 'Ladybug WindRose object is required.' f' Instead got {type(lb_wind_rose)}' # This analysis data can be Wind speed data or any other data maching the # wind direction data analysis_data = lb_wind_rose.analysis_data_collection wind_dir = lb_wind_rose.direction_data_collection if isinstance(analysis_data, HourlyDiscontinuousCollection): analysis_data = discontinuous_to_continuous(analysis_data)[0] wind_dir = discontinuous_to_continuous(wind_dir)[0] df = dataframe() series = Series(analysis_data) df['analysis_data'] = series.values series = Series(wind_dir) df['wind_dir'] = series.values start_month = lb_wind_rose.analysis_period.st_month end_month = lb_wind_rose.analysis_period.end_month start_hour = lb_wind_rose.analysis_period.st_hour end_hour = lb_wind_rose.analysis_period.end_hour if start_month <= end_month: df = df.loc[(df["month"] >= start_month) & (df["month"] <= end_month)] else: df = df.loc[(df["month"] <= end_month) | (df["month"] >= start_month)] if start_hour <= end_hour: df = df.loc[(df["hour"] >= start_hour) & (df["hour"] <= end_hour)] else: df = df.loc[(df["hour"] <= end_hour) | (df["hour"] >= start_hour)] # Use legend parameters to create the color range and labels lb_legend = Legend(analysis_data, lb_wind_rose.legend_parameters) data_bins = [round(val,2) for val in lb_legend.segment_numbers] + [np.inf] # Create a color range if the colorset does not have 11 colors if len(lb_wind_rose.legend_parameters.colors) < 11: domain = [data_bins[0], data_bins[-2]+1] color_range = ColorRange(colors=lb_wind_rose.legend_parameters.colors, domain=domain) data_colors = [rgb_to_hex(color_range.color(item)) for item in data_bins] else: data_colors = [rgb_to_hex(color) for color in lb_wind_rose.legend_parameters.colors] data_labels = _analysis_data_labels(data_bins, units=analysis_data.header.unit) dir_bins = np.arange(-22.5 / 2, 370, 22.5) dir_labels = (dir_bins[:-1] + dir_bins[1:]) / 2 total_count = df.shape[0] rose = ( df.assign( analysis_data_bins=lambda df: pd.cut( df["analysis_data"], bins=data_bins, labels=data_labels, right=True ) ) .assign( WindDir_bins=lambda df: pd.cut( df["wind_dir"], bins=dir_bins, labels=dir_labels, right=False ) ) .replace({"WindDir_bins": {360: 0}}) .groupby(by=["analysis_data_bins", "WindDir_bins"]) .size() .unstack(level="analysis_data_bins") .fillna(0) .sort_index(axis=1) .applymap(lambda x: x / total_count * 100) ) fig = go.Figure() for i, col in enumerate(rose.columns): fig.add_trace( go.Barpolar( r=rose[col], theta=rose.index.categories, name=col, marker_color=data_colors[i], hovertemplate="frequency: %{r:.2f}%" + "<br>" + "direction: %{theta:.2f}" + "\u00B0 deg" + "<br>", ) ) fig.update_traces( text=[ "North", "N-N-E", "N-E", "E-N-E", "East", "E-S-E", "S-E", "S-S-E", "South", "S-S-W", "S-W", "W-S-W", "West", "W-N-W", "N-W", "N-N-W", ] ) # setting the title for the figure if show_title: fig_title = { 'text': title if title else 'Wind Rose', 'y': 1, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_layout( autosize=True, polar_angularaxis_rotation=90, polar_angularaxis_direction="clockwise", dragmode=False, margin=dict(l=20, r=20, t=55, b=20), title=fig_title, ) return fig
[docs] def psych_chart( psych: PsychrometricChart, data: BaseCollection = None, title: str = None, show_title: bool = False, polygon_pmv: PolygonPMV = None, strategies: List[Strategy] = [Strategy.comfort], strategy_parameters: StrategyParameters = StrategyParameters(), solar_data: HourlyContinuousCollection = None, colors: List[Color] = None ) -> Figure: """Create a psychrometric chart. Args: psych: A ladybug PsychrometricChart object. data: A ladybug DataCollection object. title: A title for the plot. Defaults to None. show_title: A boolean to show or hide the title. Defaults to False. polygon_pmv: A ladybug PolygonPMV object. If provided, polygons will be drawn. Defaults to None. strategies: A list of strategies to be applied to the chart. Accepts a list of Stragegy objects. Defaults to out of the box StrategyParameters object. strategy_parameters: A StrategyParameters object. Defaults to None. solar_data: An annual hourly continuous data collection of irradiance (or radiation) in W/m2 (or Wh/m2) that aligns with the data points on the psychrometric chart. This is only required when plotting a "Passive Solar Heating" strategy polygon on the chart. The irradiance values should be incident on the orientation of the passive solar heated windows. So using global horizontal radiation assumes that all windows are skylights (like a greenhouse). Defaults to None. colors: A list of colors to be used for the comfort polygons. Defaults to None. Returns: A plotly figure. """ return _psych_chart(psych, data, title, show_title, polygon_pmv, strategies, strategy_parameters, solar_data, colors)
[docs] def sunpath( sunpath: Sunpath, data: HourlyContinuousCollection = None, colorset: Colorset = Colorset.original(), min_range: float = None, max_range: float = None, title: str = None, show_title: bool = False ) -> Figure: """ Plot Sunpath. Args: sunpath: A Ladybug Sunpath object. data: An HourlyContinuousCollection object to be plotted on the sunpath. Defaults to None. colorset: A Ladybug Colorset object. Defaults to ColorSet.original. min_range: Minimum value for the colorbar. If not set, the minimum value will be set to the minimum value of the data. Defaults to None. max_range: Maximum value for the colorbar. If not set, the maximum value will be set to the maximum value of the data. Defaults to None. title: A string to be used as the title of the plot. Defaults to None. show_title: A boolean to show or hide the title of the plot. Defaults to False. Returns: A plotly Figure. """ df = dataframe() time_zone = sunpath.time_zone altitudes, azimuths = [], [] for time in df['times']: date_time = DateTime(time.month, time.day, time.hour, time.minute) altitudes.append(sunpath.calculate_sun_from_date_time(date_time).altitude) azimuths.append(sunpath.calculate_sun_from_date_time(date_time).azimuth) df['altitude'] = altitudes df['azimuth'] = azimuths if data: assert isinstance(data, HourlyContinuousCollection), 'data must be an' f' HourlyContinuousCollection. Instead got {type(data)}.' var_name = data.header.data_type.name var_unit = data.header.unit var_colorscale = [rgb_to_hex(color) for color in colorset] chart_title = 'Sunpath - ' + var_name if title is None else title # add data to the dataframe df[var_name] = Series(data).values # filter the whole dataframe based on sun elevations solpos = df.loc[df["altitude"] > 0, :] data_max = 5 * ceil(solpos[var_name].max() / 5) data_min = 5 * floor(solpos[var_name].min() / 5) var_range = [data_min, data_max] if min_range is not None: var_range[0] = min_range if max_range is not None: var_range[1] = max_range else: solpos = df.loc[df["altitude"] > 0, :] chart_title = 'Sunpath' if title is None else title tz = "UTC" try: times = pd.date_range( "2019-01-01 00:00:00", "2020-01-01", closed="left", freq="H", tz=tz ) except TypeError: times = pd.date_range( "2019-01-01 00:00:00", "2020-01-01", inclusive="left", freq="H", tz=tz ) delta = timedelta(days=0, hours=time_zone - 1, minutes=0) times = times - delta if not data: var_color = rgb_to_hex(colorset[-1]) marker_size = 3 else: var_color = '#8c8e91' vals = solpos[var_name] marker_size = (((vals - vals.min()) / vals.max()) + 1) * 4 fig = go.Figure() # draw altitude circles for i in range(10): pt = [] for j in range(361): pt.append(j) fig.add_trace( go.Scatterpolar( r=[90 * cos(radians(i * 10))] * 361, theta=pt, mode="lines", line_color="silver", line_width=1, hovertemplate="Altitude circle<br>" + str(i * 10) + "\u00B0deg", name="", ) ) # Draw annalemma if not data: fig.add_trace( go.Scatterpolar( r=90 * np.cos(np.radians(solpos["altitude"])), theta=solpos["azimuth"], mode="markers", marker_color=var_color, marker_size=marker_size, marker_line_width=0, customdata=np.stack( ( solpos["day"], solpos["month_names"], solpos["hour"], solpos["altitude"], solpos["azimuth"], ), axis=-1, ), hovertemplate="month: %{customdata[1]}" + "<br>day: %{customdata[0]:.0f}" + "<br>hour: %{customdata[2]:.0f}:00" + "<br>sun altitude: %{customdata[3]:.2f}" + "\u00B0deg" + "<br>sun azimuth: %{customdata[4]:.2f}" + "\u00B0deg" + "<br>", name="", ) ) else: fig.add_trace( go.Scatterpolar( r=90 * np.cos(np.radians(solpos['altitude'])), theta=solpos["azimuth"], mode="markers", marker=dict( color=solpos[var_name], size=marker_size, line_width=0, colorscale=var_colorscale, cmin=var_range[0], cmax=var_range[1], colorbar=dict(thickness=10, title=var_unit + "<br> "), ), customdata=np.stack( ( solpos["day"], solpos["month_names"], solpos["hour"], solpos["altitude"], solpos["azimuth"], solpos[var_name], ), axis=-1, ), hovertemplate="month: %{customdata[1]}" + "<br>day: %{customdata[0]:.0f}" + "<br>hour: %{customdata[2]:.0f}:00" + "<br>sun altitude: %{customdata[3]:.2f}" + "\u00B0deg" + "<br>sun azimuth: %{customdata[4]:.2f}" + "\u00B0deg" + "<br>" + "<br><b>" + var_name + ": %{customdata[5]:.2f}" + var_unit + "</b>", name="", ) ) # draw equinox and sostices for date in pd.to_datetime(["2019-03-21", "2019-06-21", "2019-12-21"]): times = pd.date_range(date, date + pd.Timedelta("24h"), freq="5min", tz='UTC') times = times - delta solpos = pd.DataFrame() solpos['times'] = times solpos.set_index("times", drop=False, append=False, inplace=True, verify_integrity=False) azimuth, altitude = [], [] for time in times: azimuth.append(sunpath.calculate_sun_from_date_time( DateTime(time.month, time.day, time.hour, time.minute)).azimuth) altitude.append(sunpath.calculate_sun_from_date_time( DateTime(time.month, time.day, time.hour, time.minute)).altitude) solpos['azimuth'] = azimuth solpos['altitude'] = altitude solpos = solpos.loc[solpos['altitude'] > 0, :] # This sorting is necessary for the correct drawing of lines alts = list(90 * np.cos(np.radians(solpos.altitude))) azis = list(solpos.azimuth) azi_alt = {azis[i]: alts[i] for i in range(len(azis))} azi_alt_sorted = {k: azi_alt[k] for k in sorted(azi_alt)} fig.add_trace( go.Scatterpolar( r=list(azi_alt_sorted.values()), theta=list(azi_alt_sorted.keys()), mode="markers", marker=dict(color=var_color, size=2.5), customdata=solpos.altitude, hovertemplate="<br>sun altitude: %{customdata:.2f}" + "\u00B0deg" + "<br>sun azimuth: %{theta:.2f}" + "\u00B0deg" + "<br>", name="", ) ) # draw sunpath on the 21st of each other month for date in pd.to_datetime(["2019-01-21", "2019-02-21", "2019-4-21", "2019-5-21"]): times = pd.date_range(date, date + pd.Timedelta("24h"), freq="5min", tz=tz) times = times - delta solpos = pd.DataFrame() solpos['times'] = times solpos.set_index("times", drop=False, append=False, inplace=True, verify_integrity=False) azimuth, altitude = [], [] for time in times: azimuth.append(sunpath.calculate_sun_from_date_time( DateTime(time.month, time.day, time.hour, time.minute)).azimuth) altitude.append(sunpath.calculate_sun_from_date_time( DateTime(time.month, time.day, time.hour, time.minute)).altitude) solpos['azimuth'] = azimuth solpos['altitude'] = altitude solpos = solpos.loc[solpos["altitude"] > 0, :] # This sorting is necessary for the correct drawing of lines alts = list(90 * np.cos(np.radians(solpos.altitude))) azis = list(solpos.azimuth) azi_alt = {azis[i]: alts[i] for i in range(len(azis))} azi_alt_sorted = {k: azi_alt[k] for k in sorted(azi_alt)} fig.add_trace( go.Scatterpolar( r=list(azi_alt_sorted.values()), theta=list(azi_alt_sorted.keys()), mode="markers", marker=dict(color=var_color, size=2.5), customdata=solpos.altitude, hovertemplate="<br>sun altitude: %{customdata:.2f}" + "\u00B0deg" + "<br>sun azimuth: %{theta:.2f}" + "\u00B0deg" + "<br>", name="", ) ) # setting the title for the figure if show_title: fig_title = { 'text': chart_title, 'y': 1, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None fig.update_layout( showlegend=False, polar=dict( radialaxis=dict(tickfont_size=10, visible=False), angularaxis=dict( tickfont_size=10, rotation=90, # start position of angular axis direction="clockwise", ), ), autosize=False, template='plotly_white', title_x=0.5, dragmode=False, margin=dict(l=20, r=20, t=33, b=20), title=fig_title, ) return fig
[docs] def bar_chart_with_table(data: List[MonthlyCollection], min_range: float = None, max_range: float = None, colors: List[Color] = None, title: str = None, show_title: bool = False, stack: bool = False) -> Figure: """Create a plotly bar chart figure from multiple ladybug monthly or daily data. Args: data: A list of ladybug monthly data. min_range: Minimum value for the legend. If not set will be calculated from the data. Defaults to None. max_range: Maximum value for the legend. If not set will be calculated from the data. Defaults to None. colors: A list of ladybug color objects. The length of this list needs to match the length of data argument. If not set, random colors will be used. Defaults to None. title: A string to be used as the title of the plot. If not set, the names of data will be used to create a title for the chart. Defaults to None. show_title: A boolean to set whether to show the title of the chart. Defaults to False. stack: A boolean to determine whether to stack the data. Defaults to False which will show data side by side. Returns: A plotly figure. """ assert len(data) > 0 and all([isinstance(item, MonthlyCollection) for item in data]), 'Only a list of ladybug '\ f' monthly data is supported. Instead got {type(data)}' if colors: assert len(colors) == len(data), 'Length of colors argument needs to match'\ f' the length of data argument. Instead got {len(colors)} and {len(data)}' colors = colors if colors else [Color(randint(0, 255), randint(0, 255), randint(0, 255)) for item in data] # set the range of y-axis if provided y_range = None if min_range is not None and max_range is not None: y_range = [min_range, max_range] fig_specs = [[{"type": "bar"}], [{"type": "table"}]] fig = make_subplots(rows=2, cols=1, vertical_spacing=0.1, specs=fig_specs) names = [] for count, item in enumerate(data): var = item.header.data_type.name var_unit = item.header.unit color = colors[count] if colors else None bar = _monthly_bar(item, var, var_unit, color) fig.add_trace(bar, row=1, col=1) names.append(var) # add table values, colors = group_monthly(data, colors) table = go.Table( header=dict(values=None, fill_color='#ffffff'), cells=dict(values=values, fill_color=colors)) fig.add_trace(table, row=2, col=1) # setting the title for the figure if show_title: fig_title = { 'text': title if title else ' - '.join(names), 'y': 1, 'x': 0.5, 'xanchor': 'center', 'yanchor': 'top' } else: if title: raise ValueError( f'Title is set to "{title}" but show_title is set to False.') fig_title = None # move legend upwards as mode data is loaded legend_height = 1.2 if len(data) <= 3 else 1.2 + (len(data)-3)/10 fig.update_layout( barmode='relative' if stack else 'group', template='plotly_white', margin=dict(l=20, r=20, t=33, b=20), yaxis_nticks=13, title=fig_title, legend={ 'x': 0, 'y': legend_height, } ) fig.update_xaxes(dtick="M1", tickformat="%b", ticklabelmode="period", showline=True, linewidth=1, linecolor="black", mirror=True) fig.update_yaxes(showline=True, linewidth=1, linecolor="black", mirror=True, range=y_range) return fig