|
|
""" |
|
|
Enhanced visualization components for HVAC Load Calculator. |
|
|
This module provides improved visualization for cooling load results. |
|
|
""" |
|
|
|
|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
import seaborn as sns |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from plotly.subplots import make_subplots |
|
|
import calendar |
|
|
from models.building import Building, CoolingLoadResult |
|
|
|
|
|
def display_enhanced_results(result: CoolingLoadResult, building: Building = None, monthly_breakdown: dict = None): |
|
|
""" |
|
|
Display enhanced cooling load calculation results with improved visualizations. |
|
|
|
|
|
Args: |
|
|
result: Cooling load calculation results |
|
|
building: Optional building model for additional context |
|
|
monthly_breakdown: Optional monthly breakdown data |
|
|
""" |
|
|
st.header("Cooling Load Calculation Results") |
|
|
|
|
|
|
|
|
if building: |
|
|
st.subheader(f"Building: {building.settings.name}") |
|
|
col1, col2, col3 = st.columns(3) |
|
|
with col1: |
|
|
st.write(f"**Location:** {building.location.city}") |
|
|
st.write(f"**Latitude:** {building.location.latitude}") |
|
|
with col2: |
|
|
st.write(f"**Floor Area:** {building.settings.floor_area} m²") |
|
|
st.write(f"**Ceiling Height:** {building.settings.ceiling_height} m") |
|
|
with col3: |
|
|
st.write(f"**Indoor Temperature:** {building.settings.indoor_temp} °C") |
|
|
st.write(f"**Indoor Humidity:** {building.settings.indoor_humidity}%") |
|
|
else: |
|
|
st.subheader(f"Building: {result.building_name}") |
|
|
|
|
|
|
|
|
st.subheader("Peak Cooling Load") |
|
|
|
|
|
|
|
|
area = building.settings.floor_area if building else 100 |
|
|
sensible_per_area = result.peak_sensible_load / area |
|
|
latent_per_area = result.peak_latent_load / area |
|
|
total_per_area = result.peak_total_load / area |
|
|
|
|
|
|
|
|
|
|
|
benchmark_total = 100 |
|
|
benchmark_sensible = 75 |
|
|
benchmark_latent = 25 |
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
with col1: |
|
|
st.metric( |
|
|
"Sensible Load", |
|
|
f"{result.peak_sensible_load:.1f} W", |
|
|
f"{sensible_per_area:.1f} W/m²", |
|
|
delta_color="off" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
st.metric( |
|
|
"Latent Load", |
|
|
f"{result.peak_latent_load:.1f} W", |
|
|
f"{latent_per_area:.1f} W/m²", |
|
|
delta_color="off" |
|
|
) |
|
|
|
|
|
with col3: |
|
|
delta_percentage = ((total_per_area - benchmark_total) / benchmark_total) * 100 |
|
|
st.metric( |
|
|
"Total Load", |
|
|
f"{result.peak_total_load:.1f} W", |
|
|
f"{delta_percentage:.1f}% vs benchmark", |
|
|
delta_color="inverse" |
|
|
) |
|
|
|
|
|
with col4: |
|
|
st.metric( |
|
|
"Peak Hour", |
|
|
f"{result.peak_hour}:00", |
|
|
f"SHR: {result.peak_sensible_load / result.peak_total_load:.2f}", |
|
|
delta_color="off" |
|
|
) |
|
|
|
|
|
|
|
|
st.subheader("Load Breakdown") |
|
|
|
|
|
|
|
|
external_loads = result.external_loads |
|
|
internal_loads = result.internal_loads |
|
|
|
|
|
|
|
|
external_breakdown = { |
|
|
"Roof": external_loads.get("roof", 0), |
|
|
"Walls": external_loads.get("walls_total", 0), |
|
|
"Glass Conduction": external_loads.get("glass_conduction_total", 0), |
|
|
"Glass Solar": external_loads.get("glass_solar_total", 0) |
|
|
} |
|
|
|
|
|
|
|
|
internal_breakdown = { |
|
|
"People (Sensible)": internal_loads.get("people_sensible", 0), |
|
|
"People (Latent)": internal_loads.get("people_latent", 0), |
|
|
"Lighting": internal_loads.get("lighting", 0), |
|
|
"Equipment (Sensible)": internal_loads.get("equipment_sensible", 0), |
|
|
"Equipment (Latent)": internal_loads.get("equipment_latent", 0) |
|
|
} |
|
|
|
|
|
|
|
|
combined_breakdown = { |
|
|
"External": sum(external_breakdown.values()), |
|
|
"Internal (Sensible)": internal_loads.get("sensible_total", 0) - internal_loads.get("people_latent", 0) - internal_loads.get("equipment_latent", 0), |
|
|
"Internal (Latent)": internal_loads.get("latent_total", 0) |
|
|
} |
|
|
|
|
|
|
|
|
viz_tabs = st.tabs(["Pie Charts", "Stacked Bar", "Treemap", "Detailed Breakdown"]) |
|
|
|
|
|
with viz_tabs[0]: |
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
|
|
|
fig = px.pie( |
|
|
values=list(external_breakdown.values()), |
|
|
names=list(external_breakdown.keys()), |
|
|
title="External Loads", |
|
|
color_discrete_sequence=px.colors.qualitative.Pastel, |
|
|
hole=0.4 |
|
|
) |
|
|
fig.update_traces(textposition='inside', textinfo='percent+label') |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
with col2: |
|
|
|
|
|
fig = px.pie( |
|
|
values=list(internal_breakdown.values()), |
|
|
names=list(internal_breakdown.keys()), |
|
|
title="Internal Loads", |
|
|
color_discrete_sequence=px.colors.qualitative.Pastel2, |
|
|
hole=0.4 |
|
|
) |
|
|
fig.update_traces(textposition='inside', textinfo='percent+label') |
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
with viz_tabs[1]: |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
y=["Cooling Load"], |
|
|
x=[combined_breakdown["External"]], |
|
|
name="External", |
|
|
orientation='h', |
|
|
marker=dict(color='rgba(58, 71, 80, 0.6)') |
|
|
)) |
|
|
fig.add_trace(go.Bar( |
|
|
y=["Cooling Load"], |
|
|
x=[combined_breakdown["Internal (Sensible)"]], |
|
|
name="Internal (Sensible)", |
|
|
orientation='h', |
|
|
marker=dict(color='rgba(246, 78, 139, 0.6)') |
|
|
)) |
|
|
fig.add_trace(go.Bar( |
|
|
y=["Cooling Load"], |
|
|
x=[combined_breakdown["Internal (Latent)"]], |
|
|
name="Internal (Latent)", |
|
|
orientation='h', |
|
|
marker=dict(color='rgba(6, 147, 227, 0.6)') |
|
|
)) |
|
|
|
|
|
|
|
|
fig.update_layout( |
|
|
title="Cooling Load Composition", |
|
|
barmode='stack', |
|
|
height=300, |
|
|
xaxis=dict(title="Load (W)"), |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
total = sum(combined_breakdown.values()) |
|
|
st.write(f"**External Load:** {combined_breakdown['External']:.1f} W ({combined_breakdown['External']/total*100:.1f}%)") |
|
|
st.write(f"**Internal Sensible Load:** {combined_breakdown['Internal (Sensible)']:.1f} W ({combined_breakdown['Internal (Sensible)']/total*100:.1f}%)") |
|
|
st.write(f"**Internal Latent Load:** {combined_breakdown['Internal (Latent)']:.1f} W ({combined_breakdown['Internal (Latent)']/total*100:.1f}%)") |
|
|
|
|
|
with viz_tabs[2]: |
|
|
|
|
|
|
|
|
treemap_data = [] |
|
|
|
|
|
|
|
|
for key, value in external_breakdown.items(): |
|
|
treemap_data.append({"category": "External", "component": key, "value": value}) |
|
|
|
|
|
|
|
|
for key, value in internal_breakdown.items(): |
|
|
treemap_data.append({"category": "Internal", "component": key, "value": value}) |
|
|
|
|
|
|
|
|
treemap_df = pd.DataFrame(treemap_data) |
|
|
|
|
|
|
|
|
fig = px.treemap( |
|
|
treemap_df, |
|
|
path=['category', 'component'], |
|
|
values='value', |
|
|
title="Cooling Load Components", |
|
|
color_discrete_sequence=px.colors.qualitative.Pastel |
|
|
) |
|
|
|
|
|
|
|
|
fig.update_traces(textinfo="label+value+percent parent") |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
with viz_tabs[3]: |
|
|
|
|
|
st.write("### External Loads Breakdown") |
|
|
external_df = pd.DataFrame({ |
|
|
"Component": external_breakdown.keys(), |
|
|
"Load (W)": external_breakdown.values(), |
|
|
"Percentage (%)": [val / sum(external_breakdown.values()) * 100 for val in external_breakdown.values()] |
|
|
}) |
|
|
st.dataframe(external_df, use_container_width=True) |
|
|
|
|
|
st.write("### Internal Loads Breakdown") |
|
|
internal_df = pd.DataFrame({ |
|
|
"Component": internal_breakdown.keys(), |
|
|
"Load (W)": internal_breakdown.values(), |
|
|
"Percentage (%)": [val / sum(internal_breakdown.values()) * 100 for val in internal_breakdown.values()] |
|
|
}) |
|
|
st.dataframe(internal_df, use_container_width=True) |
|
|
|
|
|
|
|
|
if "walls" in external_loads: |
|
|
st.write("### Wall Loads by Orientation") |
|
|
wall_loads = external_loads["walls"] |
|
|
wall_df = pd.DataFrame({ |
|
|
"Wall": wall_loads.keys(), |
|
|
"Load (W)": wall_loads.values() |
|
|
}) |
|
|
st.dataframe(wall_df, use_container_width=True) |
|
|
|
|
|
|
|
|
if "glass_conduction" in external_loads and "glass_solar" in external_loads: |
|
|
st.write("### Glass Loads by Orientation") |
|
|
glass_cond = external_loads["glass_conduction"] |
|
|
glass_solar = external_loads["glass_solar"] |
|
|
|
|
|
|
|
|
glass_combined = {} |
|
|
for key in glass_cond: |
|
|
orientation = key.split("_")[2] |
|
|
if orientation not in glass_combined: |
|
|
glass_combined[orientation] = {"conduction": 0, "solar": 0} |
|
|
glass_combined[orientation]["conduction"] += glass_cond[key] |
|
|
|
|
|
for key in glass_solar: |
|
|
orientation = key.split("_")[2] |
|
|
if orientation not in glass_combined: |
|
|
glass_combined[orientation] = {"conduction": 0, "solar": 0} |
|
|
glass_combined[orientation]["solar"] += glass_solar[key] |
|
|
|
|
|
|
|
|
glass_df = pd.DataFrame([ |
|
|
{"Orientation": orientation, "Conduction (W)": data["conduction"], "Solar (W)": data["solar"], "Total (W)": data["conduction"] + data["solar"]} |
|
|
for orientation, data in glass_combined.items() |
|
|
]) |
|
|
st.dataframe(glass_df, use_container_width=True) |
|
|
|
|
|
|
|
|
st.subheader("Monthly Cooling Load Breakdown") |
|
|
|
|
|
if result.monthly_loads: |
|
|
|
|
|
months = list(result.monthly_loads.keys()) |
|
|
peak_loads = [result.monthly_loads[month]["peak_load"] for month in months] |
|
|
daily_averages = [result.monthly_loads[month]["daily_average"] for month in months] |
|
|
|
|
|
|
|
|
monthly_tabs = st.tabs(["Bar Chart", "Line Chart", "Heatmap", "Monthly Data"]) |
|
|
|
|
|
with monthly_tabs[0]: |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
x=months, |
|
|
y=peak_loads, |
|
|
name="Peak Load (W)", |
|
|
marker_color='rgb(55, 83, 109)' |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Bar( |
|
|
x=months, |
|
|
y=daily_averages, |
|
|
name="Daily Average (W)", |
|
|
marker_color='rgb(26, 118, 255)' |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title="Monthly Cooling Load Comparison", |
|
|
xaxis=dict(title="Month"), |
|
|
yaxis=dict(title="Cooling Load (W)"), |
|
|
barmode='group', |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
with monthly_tabs[1]: |
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=months, |
|
|
y=peak_loads, |
|
|
name="Peak Load (W)", |
|
|
mode='lines+markers', |
|
|
line=dict(color='rgb(55, 83, 109)', width=2), |
|
|
marker=dict(size=8) |
|
|
)) |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=months, |
|
|
y=daily_averages, |
|
|
name="Daily Average (W)", |
|
|
mode='lines+markers', |
|
|
line=dict(color='rgb(26, 118, 255)', width=2), |
|
|
marker=dict(size=8) |
|
|
)) |
|
|
|
|
|
|
|
|
if monthly_breakdown: |
|
|
monthly_temps = [monthly_breakdown[month]["avg_temp_c"] for month in months] |
|
|
|
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=months, |
|
|
y=monthly_temps, |
|
|
name="Avg. Temperature (°C)", |
|
|
mode='lines+markers', |
|
|
line=dict(color='rgb(255, 99, 71)', width=2, dash='dot'), |
|
|
marker=dict(size=8), |
|
|
yaxis="y2" |
|
|
)) |
|
|
|
|
|
|
|
|
fig.update_layout( |
|
|
yaxis2=dict( |
|
|
title="Temperature (°C)", |
|
|
overlaying="y", |
|
|
side="right", |
|
|
range=[0, max(monthly_temps) * 1.2] |
|
|
) |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
title="Monthly Cooling Load Trends", |
|
|
xaxis=dict(title="Month"), |
|
|
yaxis=dict(title="Cooling Load (W)"), |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
with monthly_tabs[2]: |
|
|
|
|
|
if all("hourly_loads" in result.monthly_loads[month] for month in months): |
|
|
|
|
|
hourly_data = np.zeros((12, 24)) |
|
|
|
|
|
for i, month in enumerate(months): |
|
|
hourly_data[i] = result.monthly_loads[month]["hourly_loads"] |
|
|
|
|
|
|
|
|
fig = px.imshow( |
|
|
hourly_data, |
|
|
labels=dict(x="Hour of Day", y="Month", color="Cooling Load (W)"), |
|
|
x=list(range(1, 25)), |
|
|
y=months, |
|
|
color_continuous_scale="Viridis", |
|
|
title="Hourly Cooling Load by Month" |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
xaxis=dict(tickmode='linear', tick0=1, dtick=1), |
|
|
coloraxis_colorbar=dict(title="Load (W)") |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.info("This heatmap shows the cooling load for each hour of the day across different months. Darker colors indicate higher cooling loads.") |
|
|
else: |
|
|
st.info("Hourly load data not available for heatmap visualization.") |
|
|
|
|
|
with monthly_tabs[3]: |
|
|
|
|
|
if monthly_breakdown: |
|
|
|
|
|
monthly_df = pd.DataFrame([ |
|
|
{ |
|
|
"Month": month, |
|
|
"Avg. Temp (°C)": data["avg_temp_c"], |
|
|
"Peak Load (kW)": data["peak_load_w"] / 1000, |
|
|
"Avg. Load (kW)": data["average_load_w"] / 1000, |
|
|
"Energy (kWh)": data["energy_kwh"], |
|
|
"CDD": data["cooling_degree_days"] |
|
|
} |
|
|
for month, data in monthly_breakdown.items() if month != "annual" |
|
|
]) |
|
|
|
|
|
|
|
|
annual = monthly_breakdown["annual"] |
|
|
monthly_df = monthly_df.append({ |
|
|
"Month": "Annual", |
|
|
"Avg. Temp (°C)": annual["avg_temp_c"], |
|
|
"Peak Load (kW)": annual["peak_load_w"] / 1000, |
|
|
"Avg. Load (kW)": annual["average_load_w"] / 1000, |
|
|
"Energy (kWh)": annual["energy_kwh"], |
|
|
"CDD": annual["cooling_degree_days"] |
|
|
}, ignore_index=True) |
|
|
|
|
|
st.dataframe(monthly_df, use_container_width=True) |
|
|
|
|
|
|
|
|
st.info("CDD = Cooling Degree Days (base 18°C)") |
|
|
else: |
|
|
|
|
|
monthly_df = pd.DataFrame({ |
|
|
"Month": months, |
|
|
"Peak Load (W)": peak_loads, |
|
|
"Daily Average (W)": daily_averages |
|
|
}) |
|
|
st.dataframe(monthly_df, use_container_width=True) |
|
|
else: |
|
|
st.info("Monthly breakdown data not available.") |
|
|
|
|
|
|
|
|
st.subheader("Hourly Load Profile for Peak Day") |
|
|
|
|
|
if result.monthly_loads and result.peak_hour: |
|
|
|
|
|
peak_month = max(result.monthly_loads.keys(), key=lambda m: result.monthly_loads[m]["peak_load"]) |
|
|
|
|
|
|
|
|
if "hourly_loads" in result.monthly_loads[peak_month]: |
|
|
hourly_loads = result.monthly_loads[peak_month]["hourly_loads"] |
|
|
|
|
|
|
|
|
fig = go.Figure() |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=list(range(1, 25)), |
|
|
y=hourly_loads, |
|
|
name="Cooling Load", |
|
|
mode='lines+markers', |
|
|
line=dict(color='rgb(26, 118, 255)', width=3), |
|
|
marker=dict(size=8) |
|
|
)) |
|
|
|
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=[result.peak_hour], |
|
|
y=[hourly_loads[result.peak_hour - 1]], |
|
|
name="Peak Hour", |
|
|
mode='markers', |
|
|
marker=dict(color='red', size=12, symbol='star') |
|
|
)) |
|
|
|
|
|
|
|
|
if monthly_breakdown and peak_month in monthly_breakdown: |
|
|
|
|
|
avg_temp = monthly_breakdown[peak_month]["avg_temp_c"] |
|
|
|
|
|
|
|
|
hourly_temps = [ |
|
|
avg_temp - 3 + 6 * np.sin(np.pi * (hour - 4) / 12) |
|
|
for hour in range(1, 25) |
|
|
] |
|
|
|
|
|
fig.add_trace(go.Scatter( |
|
|
x=list(range(1, 25)), |
|
|
y=hourly_temps, |
|
|
name="Temperature (°C)", |
|
|
mode='lines', |
|
|
line=dict(color='rgb(255, 99, 71)', width=2, dash='dot'), |
|
|
yaxis="y2" |
|
|
)) |
|
|
|
|
|
|
|
|
fig.update_layout( |
|
|
yaxis2=dict( |
|
|
title="Temperature (°C)", |
|
|
overlaying="y", |
|
|
side="right", |
|
|
range=[min(hourly_temps) - 2, max(hourly_temps) + 2] |
|
|
) |
|
|
) |
|
|
|
|
|
fig.update_layout( |
|
|
title=f"Hourly Cooling Load Profile for {peak_month}", |
|
|
xaxis=dict(title="Hour of Day", tickmode='linear', tick0=1, dtick=1), |
|
|
yaxis=dict(title="Cooling Load (W)"), |
|
|
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1) |
|
|
) |
|
|
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
|
|
|
st.info(f"The peak cooling load occurs at {result.peak_hour}:00 in {peak_month} with a value of {hourly_loads[result.peak_hour - 1]:.1f} W.") |
|
|
else: |
|
|
st.info("Hourly load profile data not available.") |
|
|
else: |
|
|
st.info("Hourly load profile data not available.") |
|
|
|
|
|
|
|
|
if monthly_breakdown and "annual" in monthly_breakdown: |
|
|
st.subheader("Energy Efficiency Recommendations") |
|
|
|
|
|
annual = monthly_breakdown["annual"] |
|
|
annual_energy_per_m2 = annual["energy_kwh"] / building.settings.floor_area if building else 0 |
|
|
peak_load_per_m2 = annual["peak_load_w"] / building.settings.floor_area if building else 0 |
|
|
|
|
|
|
|
|
recommendations = [] |
|
|
|
|
|
if annual_energy_per_m2 > 100: |
|
|
recommendations.append(f"The annual cooling energy consumption ({annual_energy_per_m2:.1f} kWh/m²) is high. Consider improving building envelope insulation.") |
|
|
|
|
|
if peak_load_per_m2 > 100: |
|
|
recommendations.append(f"The peak cooling load ({peak_load_per_m2:.1f} W/m²) is high. Consider reducing solar heat gain through windows.") |
|
|
|
|
|
if building and building.settings.indoor_temp < 24: |
|
|
recommendations.append(f"The indoor design temperature ({building.settings.indoor_temp} °C) is low. Increasing it to 24-26 °C could reduce cooling energy consumption.") |
|
|
|
|
|
if building and any(glass.shgc > 0.4 for glass in building.glass): |
|
|
recommendations.append("Consider using low-SHGC glazing (SHGC < 0.4) to reduce solar heat gain through windows.") |
|
|
|
|
|
if building and any(wall.u_value > 0.5 for wall in building.walls): |
|
|
recommendations.append("Some walls have high U-values. Consider adding insulation to reduce heat gain through walls.") |
|
|
|
|
|
if building and building.roof.u_value > 0.3: |
|
|
recommendations.append("The roof has a high U-value. Consider adding roof insulation to reduce heat gain.") |
|
|
|
|
|
|
|
|
if recommendations: |
|
|
for i, recommendation in enumerate(recommendations): |
|
|
st.write(f"{i+1}. {recommendation}") |
|
|
else: |
|
|
st.write("No specific recommendations available based on the current data.") |
|
|
|
|
|
|
|
|
st.subheader("Additional Information") |
|
|
|
|
|
|
|
|
st.write(f"Calculation performed on: {result.timestamp.strftime('%Y-%m-%d %H:%M:%S')}") |
|
|
|
|
|
|
|
|
st.info("Results are based on enhanced ASHRAE cooling load calculation methods with improved accuracy.") |
|
|
|
|
|
|
|
|
st.subheader("Export Options") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
if st.button("Export to CSV"): |
|
|
|
|
|
st.success("Export functionality will be implemented in a future update.") |
|
|
|
|
|
with col2: |
|
|
if st.button("Export to PDF"): |
|
|
|
|
|
st.success("Export functionality will be implemented in a future update.") |
|
|
|
|
|
with col3: |
|
|
if st.button("Export Monthly Report"): |
|
|
|
|
|
st.success("Export functionality will be implemented in a future update.") |
|
|
|