"""
Time-based visualization module for HVAC Load Calculator.
This module provides visualization tools for time-based load analysis.
"""
import streamlit as st
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
from typing import Dict, List, Any, Optional, Tuple
import math
import calendar
from datetime import datetime, timedelta
class TimeBasedVisualization:
"""Class for time-based visualization."""
@staticmethod
def create_hourly_load_profile(hourly_loads: Dict[str, List[float]],
date: str = "Jul 15") -> go.Figure:
"""
Create an hourly load profile chart.
Args:
hourly_loads: Dictionary with hourly load data
date: Date for the profile (e.g., "Jul 15")
Returns:
Plotly figure with hourly load profile
"""
# Create hour labels
hours = list(range(24))
hour_labels = [f"{h}:00" for h in hours]
# Create figure
fig = go.Figure()
# Add total load trace
if "total" in hourly_loads:
fig.add_trace(go.Scatter(
x=hour_labels,
y=hourly_loads["total"],
mode="lines+markers",
name="Total Load",
line=dict(color="rgba(55, 83, 109, 1)", width=3),
marker=dict(size=8)
))
# Add component load traces
for component, loads in hourly_loads.items():
if component == "total":
continue
# Format component name for display
display_name = component.replace("_", " ").title()
fig.add_trace(go.Scatter(
x=hour_labels,
y=loads,
mode="lines+markers",
name=display_name,
marker=dict(size=6),
line=dict(width=2)
))
# Update layout
fig.update_layout(
title=f"Hourly Load Profile ({date})",
xaxis_title="Hour of Day",
yaxis_title="Load (W)",
height=500,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
hovermode="x unified"
)
return fig
@staticmethod
def create_daily_load_profile(daily_loads: Dict[str, List[float]],
month: str = "July") -> go.Figure:
"""
Create a daily load profile chart for a month.
Args:
daily_loads: Dictionary with daily load data
month: Month name
Returns:
Plotly figure with daily load profile
"""
# Get number of days in month
month_num = list(calendar.month_name).index(month)
year = datetime.now().year
num_days = calendar.monthrange(year, month_num)[1]
# Create day labels
days = list(range(1, num_days + 1))
day_labels = [f"{d}" for d in days]
# Create figure
fig = go.Figure()
# Add total load trace
if "total" in daily_loads:
fig.add_trace(go.Scatter(
x=day_labels,
y=daily_loads["total"][:num_days],
mode="lines+markers",
name="Total Load",
line=dict(color="rgba(55, 83, 109, 1)", width=3),
marker=dict(size=8)
))
# Add component load traces
for component, loads in daily_loads.items():
if component == "total":
continue
# Format component name for display
display_name = component.replace("_", " ").title()
fig.add_trace(go.Scatter(
x=day_labels,
y=loads[:num_days],
mode="lines+markers",
name=display_name,
marker=dict(size=6),
line=dict(width=2)
))
# Update layout
fig.update_layout(
title=f"Daily Load Profile ({month})",
xaxis_title="Day of Month",
yaxis_title="Load (W)",
height=500,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
hovermode="x unified"
)
return fig
@staticmethod
def create_monthly_load_comparison(monthly_loads: Dict[str, List[float]],
load_type: str = "cooling") -> go.Figure:
"""
Create a monthly load comparison chart.
Args:
monthly_loads: Dictionary with monthly load data
load_type: Type of load ("cooling" or "heating")
Returns:
Plotly figure with monthly load comparison
"""
# Create month labels
months = list(calendar.month_name)[1:]
# Create figure
fig = go.Figure()
# Add total load bars
if "total" in monthly_loads:
fig.add_trace(go.Bar(
x=months,
y=monthly_loads["total"],
name="Total Load",
marker_color="rgba(55, 83, 109, 0.7)",
opacity=0.7
))
# Add component load bars
for component, loads in monthly_loads.items():
if component == "total":
continue
# Format component name for display
display_name = component.replace("_", " ").title()
fig.add_trace(go.Bar(
x=months,
y=loads,
name=display_name,
visible="legendonly"
))
# Update layout
title = f"Monthly {load_type.title()} Load Comparison"
y_title = f"{load_type.title()} Load (kWh)"
fig.update_layout(
title=title,
xaxis_title="Month",
yaxis_title=y_title,
height=500,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
hovermode="x unified"
)
return fig
@staticmethod
def create_annual_load_distribution(annual_loads: Dict[str, float],
load_type: str = "cooling") -> go.Figure:
"""
Create an annual load distribution pie chart.
Args:
annual_loads: Dictionary with annual load data by component
load_type: Type of load ("cooling" or "heating")
Returns:
Plotly figure with annual load distribution
"""
# Extract components and values
components = []
values = []
for component, load in annual_loads.items():
if component == "total":
continue
# Format component name for display
display_name = component.replace("_", " ").title()
components.append(display_name)
values.append(load)
# Create pie chart
fig = go.Figure(data=[go.Pie(
labels=components,
values=values,
hole=0.3,
textinfo="label+percent",
insidetextorientation="radial"
)])
# Update layout
title = f"Annual {load_type.title()} Load Distribution"
fig.update_layout(
title=title,
height=500,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
)
)
return fig
@staticmethod
def create_peak_load_analysis(peak_loads: Dict[str, Dict[str, Any]],
load_type: str = "cooling") -> go.Figure:
"""
Create a peak load analysis chart.
Args:
peak_loads: Dictionary with peak load data
load_type: Type of load ("cooling" or "heating")
Returns:
Plotly figure with peak load analysis
"""
# Extract peak load data
components = []
values = []
times = []
for component, data in peak_loads.items():
if component == "total":
continue
# Format component name for display
display_name = component.replace("_", " ").title()
components.append(display_name)
values.append(data["value"])
times.append(data["time"])
# Create bar chart
fig = go.Figure(data=[go.Bar(
x=components,
y=values,
text=times,
textposition="auto",
hovertemplate="%{x}
Peak Load: %{y:.0f} W
Time: %{text}"
)])
# Update layout
title = f"Peak {load_type.title()} Load Analysis"
y_title = f"Peak {load_type.title()} Load (W)"
fig.update_layout(
title=title,
xaxis_title="Component",
yaxis_title=y_title,
height=500
)
return fig
@staticmethod
def create_load_duration_curve(hourly_loads: List[float],
load_type: str = "cooling") -> go.Figure:
"""
Create a load duration curve.
Args:
hourly_loads: List of hourly loads for the year
load_type: Type of load ("cooling" or "heating")
Returns:
Plotly figure with load duration curve
"""
# Sort loads in descending order
sorted_loads = sorted(hourly_loads, reverse=True)
# Create hour indices
hours = list(range(1, len(sorted_loads) + 1))
# Create figure
fig = go.Figure(data=[go.Scatter(
x=hours,
y=sorted_loads,
mode="lines",
line=dict(color="rgba(55, 83, 109, 1)", width=2),
fill="tozeroy",
fillcolor="rgba(55, 83, 109, 0.2)"
)])
# Update layout
title = f"{load_type.title()} Load Duration Curve"
x_title = "Hours"
y_title = f"{load_type.title()} Load (W)"
fig.update_layout(
title=title,
xaxis_title=x_title,
yaxis_title=y_title,
height=500,
xaxis=dict(
type="log",
range=[0, math.log10(len(hours))]
)
)
return fig
@staticmethod
def create_heat_map(hourly_data: List[List[float]],
x_labels: List[str],
y_labels: List[str],
title: str,
colorscale: str = "Viridis") -> go.Figure:
"""
Create a heat map visualization.
Args:
hourly_data: 2D list of hourly data
x_labels: Labels for x-axis
y_labels: Labels for y-axis
title: Chart title
colorscale: Colorscale for the heatmap
Returns:
Plotly figure with heat map
"""
# Create figure
fig = go.Figure(data=go.Heatmap(
z=hourly_data,
x=x_labels,
y=y_labels,
colorscale=colorscale,
colorbar=dict(title="Load (W)")
))
# Update layout
fig.update_layout(
title=title,
height=600,
xaxis=dict(
title="Hour of Day",
tickmode="array",
tickvals=list(range(0, 24, 2)),
ticktext=[f"{h}:00" for h in range(0, 24, 2)]
),
yaxis=dict(
title="Day",
autorange="reversed"
)
)
return fig
@staticmethod
def display_time_based_visualization(cooling_loads: Dict[str, Any] = None,
heating_loads: Dict[str, Any] = None) -> None:
"""
Display time-based visualization in Streamlit.
Args:
cooling_loads: Dictionary with cooling load data
heating_loads: Dictionary with heating load data
"""
st.header("Time-Based Visualization")
# Check if load data exists
if cooling_loads is None and heating_loads is None:
st.warning("No load data available for visualization.")
# Create sample data for demonstration
st.info("Using sample data for demonstration.")
# Generate sample cooling loads
cooling_loads = {
"hourly": {
"total": [1000 + 500 * math.sin(h * math.pi / 12) + 1000 * math.sin(h * math.pi / 6) for h in range(24)],
"walls": [300 + 150 * math.sin(h * math.pi / 12) for h in range(24)],
"roofs": [400 + 200 * math.sin(h * math.pi / 12) for h in range(24)],
"windows": [500 + 300 * math.sin(h * math.pi / 6) for h in range(24)],
"internal": [200 + 100 * math.sin(h * math.pi / 8) for h in range(24)]
},
"daily": {
"total": [2000 + 1000 * math.sin(d * math.pi / 15) for d in range(1, 32)],
"walls": [600 + 300 * math.sin(d * math.pi / 15) for d in range(1, 32)],
"roofs": [800 + 400 * math.sin(d * math.pi / 15) for d in range(1, 32)],
"windows": [1000 + 500 * math.sin(d * math.pi / 15) for d in range(1, 32)]
},
"monthly": {
"total": [1000, 1200, 1500, 2000, 2500, 3000, 3500, 3200, 2800, 2000, 1500, 1200],
"walls": [300, 350, 400, 500, 600, 700, 800, 750, 650, 500, 400, 350],
"roofs": [400, 450, 500, 600, 700, 800, 900, 850, 750, 600, 500, 450],
"windows": [500, 550, 600, 700, 800, 900, 1000, 950, 850, 700, 600, 550]
},
"annual": {
"total": 25000,
"walls": 6000,
"roofs": 8000,
"windows": 9000,
"internal": 2000
},
"peak": {
"total": {"value": 3500, "time": "Jul 15, 15:00"},
"walls": {"value": 800, "time": "Jul 15, 16:00"},
"roofs": {"value": 900, "time": "Jul 15, 14:00"},
"windows": {"value": 1000, "time": "Jul 15, 15:00"},
"internal": {"value": 200, "time": "Jul 15, 17:00"}
}
}
# Generate sample heating loads
heating_loads = {
"hourly": {
"total": [3000 - 1000 * math.sin(h * math.pi / 12) for h in range(24)],
"walls": [900 - 300 * math.sin(h * math.pi / 12) for h in range(24)],
"roofs": [1200 - 400 * math.sin(h * math.pi / 12) for h in range(24)],
"windows": [1500 - 500 * math.sin(h * math.pi / 12) for h in range(24)]
},
"daily": {
"total": [3000 - 1000 * math.sin(d * math.pi / 15) for d in range(1, 32)],
"walls": [900 - 300 * math.sin(d * math.pi / 15) for d in range(1, 32)],
"roofs": [1200 - 400 * math.sin(d * math.pi / 15) for d in range(1, 32)],
"windows": [1500 - 500 * math.sin(d * math.pi / 15) for d in range(1, 32)]
},
"monthly": {
"total": [3500, 3200, 2800, 2000, 1500, 1000, 800, 1000, 1500, 2000, 2800, 3500],
"walls": [1050, 960, 840, 600, 450, 300, 240, 300, 450, 600, 840, 1050],
"roofs": [1400, 1280, 1120, 800, 600, 400, 320, 400, 600, 800, 1120, 1400],
"windows": [1750, 1600, 1400, 1000, 750, 500, 400, 500, 750, 1000, 1400, 1750]
},
"annual": {
"total": 25000,
"walls": 7500,
"roofs": 10000,
"windows": 12500,
"infiltration": 5000
},
"peak": {
"total": {"value": 3500, "time": "Jan 15, 06:00"},
"walls": {"value": 1050, "time": "Jan 15, 06:00"},
"roofs": {"value": 1400, "time": "Jan 15, 06:00"},
"windows": {"value": 1750, "time": "Jan 15, 06:00"},
"infiltration": {"value": 500, "time": "Jan 15, 06:00"}
}
}
# Create tabs for different visualizations
tab1, tab2, tab3, tab4, tab5 = st.tabs([
"Hourly Profiles",
"Monthly Comparison",
"Annual Distribution",
"Peak Load Analysis",
"Heat Maps"
])
with tab1:
st.subheader("Hourly Load Profiles")
# Add load type selector
load_type = st.radio(
"Select Load Type",
["cooling", "heating"],
horizontal=True,
key="hourly_profile_type"
)
# Add date selector
date = st.selectbox(
"Select Date",
["Jan 15", "Apr 15", "Jul 15", "Oct 15"],
index=2,
key="hourly_profile_date"
)
# Get appropriate load data
if load_type == "cooling":
hourly_data = cooling_loads.get("hourly", {})
else:
hourly_data = heating_loads.get("hourly", {})
# Create and display chart
fig = TimeBasedVisualization.create_hourly_load_profile(hourly_data, date)
st.plotly_chart(fig, use_container_width=True)
# Add daily profile option
st.subheader("Daily Load Profiles")
# Add month selector
month = st.selectbox(
"Select Month",
list(calendar.month_name)[1:],
index=6, # July
key="daily_profile_month"
)
# Get appropriate load data
if load_type == "cooling":
daily_data = cooling_loads.get("daily", {})
else:
daily_data = heating_loads.get("daily", {})
# Create and display chart
fig = TimeBasedVisualization.create_daily_load_profile(daily_data, month)
st.plotly_chart(fig, use_container_width=True)
with tab2:
st.subheader("Monthly Load Comparison")
# Add load type selector
load_type = st.radio(
"Select Load Type",
["cooling", "heating"],
horizontal=True,
key="monthly_comparison_type"
)
# Get appropriate load data
if load_type == "cooling":
monthly_data = cooling_loads.get("monthly", {})
else:
monthly_data = heating_loads.get("monthly", {})
# Create and display chart
fig = TimeBasedVisualization.create_monthly_load_comparison(monthly_data, load_type)
st.plotly_chart(fig, use_container_width=True)
# Add download button for CSV
monthly_df = pd.DataFrame(monthly_data)
monthly_df.index = list(calendar.month_name)[1:]
csv = monthly_df.to_csv().encode('utf-8')
st.download_button(
label=f"Download Monthly {load_type.title()} Loads as CSV",
data=csv,
file_name=f"monthly_{load_type}_loads.csv",
mime="text/csv"
)
with tab3:
st.subheader("Annual Load Distribution")
# Add load type selector
load_type = st.radio(
"Select Load Type",
["cooling", "heating"],
horizontal=True,
key="annual_distribution_type"
)
# Get appropriate load data
if load_type == "cooling":
annual_data = cooling_loads.get("annual", {})
else:
annual_data = heating_loads.get("annual", {})
# Create and display chart
fig = TimeBasedVisualization.create_annual_load_distribution(annual_data, load_type)
st.plotly_chart(fig, use_container_width=True)
# Display annual total
total = annual_data.get("total", 0)
st.metric(f"Total Annual {load_type.title()} Load", f"{total:,.0f} kWh")
# Add download button for CSV
annual_df = pd.DataFrame({"Component": list(annual_data.keys()), "Load (kWh)": list(annual_data.values())})
csv = annual_df.to_csv(index=False).encode('utf-8')
st.download_button(
label=f"Download Annual {load_type.title()} Loads as CSV",
data=csv,
file_name=f"annual_{load_type}_loads.csv",
mime="text/csv"
)
with tab4:
st.subheader("Peak Load Analysis")
# Add load type selector
load_type = st.radio(
"Select Load Type",
["cooling", "heating"],
horizontal=True,
key="peak_load_type"
)
# Get appropriate load data
if load_type == "cooling":
peak_data = cooling_loads.get("peak", {})
else:
peak_data = heating_loads.get("peak", {})
# Create and display chart
fig = TimeBasedVisualization.create_peak_load_analysis(peak_data, load_type)
st.plotly_chart(fig, use_container_width=True)
# Display peak total
peak_total = peak_data.get("total", {}).get("value", 0)
peak_time = peak_data.get("total", {}).get("time", "")
st.metric(f"Peak {load_type.title()} Load", f"{peak_total:,.0f} W")
st.write(f"Peak Time: {peak_time}")
# Add download button for CSV
peak_df = pd.DataFrame({
"Component": list(peak_data.keys()),
"Peak Load (W)": [data.get("value", 0) for data in peak_data.values()],
"Time": [data.get("time", "") for data in peak_data.values()]
})
csv = peak_df.to_csv(index=False).encode('utf-8')
st.download_button(
label=f"Download Peak {load_type.title()} Loads as CSV",
data=csv,
file_name=f"peak_{load_type}_loads.csv",
mime="text/csv"
)
with tab5:
st.subheader("Heat Maps")
# Add load type selector
load_type = st.radio(
"Select Load Type",
["cooling", "heating"],
horizontal=True,
key="heat_map_type"
)
# Add month selector
month = st.selectbox(
"Select Month",
list(calendar.month_name)[1:],
index=6, # July
key="heat_map_month"
)
# Generate heat map data
month_num = list(calendar.month_name).index(month)
year = datetime.now().year
num_days = calendar.monthrange(year, month_num)[1]
# Get appropriate hourly data
if load_type == "cooling":
hourly_data = cooling_loads.get("hourly", {}).get("total", [])
else:
hourly_data = heating_loads.get("hourly", {}).get("total", [])
# Create 2D array for heat map
heat_map_data = []
for day in range(1, num_days + 1):
# Generate hourly data with day-to-day variation
day_factor = 1 + 0.2 * math.sin(day * math.pi / 15)
day_data = [load * day_factor for load in hourly_data]
heat_map_data.append(day_data)
# Create hour and day labels
hour_labels = list(range(24))
day_labels = list(range(1, num_days + 1))
# Create and display heat map
title = f"{load_type.title()} Load Heat Map ({month})"
colorscale = "Hot" if load_type == "cooling" else "Ice"
fig = TimeBasedVisualization.create_heat_map(heat_map_data, hour_labels, day_labels, title, colorscale)
st.plotly_chart(fig, use_container_width=True)
# Add explanation
st.info(
"The heat map shows the hourly load pattern for each day of the selected month. "
"Darker colors indicate higher loads. This visualization helps identify peak load periods "
"and daily/weekly patterns."
)
# Create a singleton instance
time_based_visualization = TimeBasedVisualization()
# Example usage
if __name__ == "__main__":
import streamlit as st
# Display time-based visualization with sample data
time_based_visualization.display_time_based_visualization()