mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2025-04-19 08:55:15 +00:00
Visualisation footer with current date and time (#364)
* footer with date and version * ruff * replace toml module with build in * using re to extract the string * optimize re usage * use of use pendulum in Akkudoktor-EOS * create_line_chart_date function added * replace datetime with pendulum * align ax2 with ax1 and 0 first point * dynamic ticks * all charts with dates * style changes * mypy fixes * fix test * fixed current time
This commit is contained in:
parent
d317aa9937
commit
745086c2eb
@ -5,19 +5,22 @@ import textwrap
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import Callable, Optional, Union
|
from typing import Callable, Optional, Union
|
||||||
|
|
||||||
|
import matplotlib.dates as mdates
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pendulum
|
||||||
from matplotlib.backends.backend_pdf import PdfPages
|
from matplotlib.backends.backend_pdf import PdfPages
|
||||||
|
|
||||||
from akkudoktoreos.core.coreabc import ConfigMixin
|
from akkudoktoreos.core.coreabc import ConfigMixin
|
||||||
from akkudoktoreos.core.logging import get_logger
|
from akkudoktoreos.core.logging import get_logger
|
||||||
from akkudoktoreos.optimization.genetic import OptimizationParameters
|
from akkudoktoreos.optimization.genetic import OptimizationParameters
|
||||||
|
from akkudoktoreos.utils.datetimeutil import to_datetime
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VisualizationReport(ConfigMixin):
|
class VisualizationReport(ConfigMixin):
|
||||||
def __init__(self, filename: str = "visualization_results.pdf") -> None:
|
def __init__(self, filename: str = "visualization_results.pdf", version: str = "0.0.1") -> None:
|
||||||
# Initialize the report with a given filename and empty groups
|
# Initialize the report with a given filename and empty groups
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.groups: list[list[Callable[[], None]]] = [] # Store groups of charts
|
self.groups: list[list[Callable[[], None]]] = [] # Store groups of charts
|
||||||
@ -25,6 +28,8 @@ class VisualizationReport(ConfigMixin):
|
|||||||
Callable[[], None]
|
Callable[[], None]
|
||||||
] = [] # Store current group of charts being created
|
] = [] # Store current group of charts being created
|
||||||
self.pdf_pages = PdfPages(filename, metadata={}) # Initialize PdfPages without metadata
|
self.pdf_pages = PdfPages(filename, metadata={}) # Initialize PdfPages without metadata
|
||||||
|
self.version = version # overwrite version as test for constant output of pdf for test
|
||||||
|
self.current_time = to_datetime(as_string="YYYY-MM-DD HH:mm:ss")
|
||||||
|
|
||||||
def add_chart_to_group(self, chart_func: Callable[[], None]) -> None:
|
def add_chart_to_group(self, chart_func: Callable[[], None]) -> None:
|
||||||
"""Add a chart function to the current group."""
|
"""Add a chart function to the current group."""
|
||||||
@ -81,6 +86,20 @@ class VisualizationReport(ConfigMixin):
|
|||||||
fig, axs = plt.subplots(rows, cols, figsize=(14, 7 * rows))
|
fig, axs = plt.subplots(rows, cols, figsize=(14, 7 * rows))
|
||||||
axs = list(np.array(axs).reshape(-1))
|
axs = list(np.array(axs).reshape(-1))
|
||||||
|
|
||||||
|
# Add footer text with current time to each page
|
||||||
|
if self.version == "test":
|
||||||
|
current_time = "test"
|
||||||
|
else:
|
||||||
|
current_time = self.current_time
|
||||||
|
fig.text(
|
||||||
|
0.5,
|
||||||
|
0.02,
|
||||||
|
f"Generated on: {current_time} with version: {self.version}",
|
||||||
|
ha="center",
|
||||||
|
va="center",
|
||||||
|
fontsize=10,
|
||||||
|
)
|
||||||
|
|
||||||
# Render each chart in its corresponding axis
|
# Render each chart in its corresponding axis
|
||||||
for idx, chart_func in enumerate(group):
|
for idx, chart_func in enumerate(group):
|
||||||
plt.sca(axs[idx]) # Set current axis
|
plt.sca(axs[idx]) # Set current axis
|
||||||
@ -93,6 +112,85 @@ class VisualizationReport(ConfigMixin):
|
|||||||
self.pdf_pages.savefig(fig) # Save the figure to the PDF
|
self.pdf_pages.savefig(fig) # Save the figure to the PDF
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
|
def create_line_chart_date(
|
||||||
|
self,
|
||||||
|
start_date: pendulum.DateTime,
|
||||||
|
y_list: list[Union[np.ndarray, list[Optional[float]], list[float]]],
|
||||||
|
ylabel: str,
|
||||||
|
xlabel: Optional[str] = None,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
labels: Optional[list[str]] = None,
|
||||||
|
markers: Optional[list[str]] = None,
|
||||||
|
line_styles: Optional[list[str]] = None,
|
||||||
|
x2label: Optional[Union[str, None]] = "Hours Since Start",
|
||||||
|
) -> None:
|
||||||
|
"""Create a line chart and add it to the current group."""
|
||||||
|
|
||||||
|
def chart() -> None:
|
||||||
|
timestamps = [
|
||||||
|
start_date.add(hours=i) for i in range(len(y_list[0]))
|
||||||
|
] # 840 timestamps at 1-hour intervals
|
||||||
|
|
||||||
|
for idx, y_data in enumerate(y_list):
|
||||||
|
label = labels[idx] if labels else None # Chart label
|
||||||
|
marker = markers[idx] if markers and idx < len(markers) else "o" # Marker style
|
||||||
|
line_style = line_styles[idx] if line_styles and idx < len(line_styles) else "-"
|
||||||
|
plt.plot(
|
||||||
|
timestamps, y_data, label=label, marker=marker, linestyle=line_style
|
||||||
|
) # Plot line
|
||||||
|
|
||||||
|
# Format the time axis
|
||||||
|
plt.gca().xaxis.set_major_formatter(
|
||||||
|
mdates.DateFormatter("%Y-%m-%d")
|
||||||
|
) # Show date and time
|
||||||
|
plt.gca().xaxis.set_major_locator(
|
||||||
|
mdates.DayLocator(interval=1, tz=None)
|
||||||
|
) # Major ticks every day
|
||||||
|
plt.gca().xaxis.set_minor_locator(mdates.HourLocator(interval=3, tz=None))
|
||||||
|
# Minor ticks every 6 hours
|
||||||
|
plt.gca().xaxis.set_minor_formatter(mdates.DateFormatter("%H"))
|
||||||
|
# plt.gcf().autofmt_xdate(rotation=45, which="major")
|
||||||
|
# Auto-format the x-axis for readability
|
||||||
|
|
||||||
|
# Move major tick labels further down to avoid collision with minor tick labels
|
||||||
|
for plt_label in plt.gca().get_xticklabels(which="major"):
|
||||||
|
plt_label.set_y(-0.04)
|
||||||
|
|
||||||
|
# Add labels, title, and legend
|
||||||
|
if xlabel:
|
||||||
|
plt.xlabel(xlabel)
|
||||||
|
plt.ylabel(ylabel)
|
||||||
|
if title:
|
||||||
|
plt.title(title)
|
||||||
|
if labels:
|
||||||
|
plt.legend()
|
||||||
|
plt.grid(True)
|
||||||
|
|
||||||
|
# Add vertical line for the current date if within the axis range
|
||||||
|
current_time = pendulum.now()
|
||||||
|
if timestamps[0].subtract(hours=2) <= current_time <= timestamps[-1]:
|
||||||
|
plt.axvline(current_time, color="r", linestyle="--", label="Now")
|
||||||
|
plt.text(current_time, plt.ylim()[1], "Now", color="r", ha="center", va="bottom")
|
||||||
|
|
||||||
|
# Add a second x-axis on top
|
||||||
|
ax1 = plt.gca()
|
||||||
|
ax2 = ax1.twiny()
|
||||||
|
ax2.set_xlim(ax1.get_xlim()) # Align the second axis with the first
|
||||||
|
|
||||||
|
# Generate integer hour labels
|
||||||
|
hours_since_start = [(t - timestamps[0]).total_seconds() / 3600 for t in timestamps]
|
||||||
|
# ax2.set_xticks(timestamps[::48]) # Set ticks every 12 hours
|
||||||
|
# ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[::48]])
|
||||||
|
ax2.set_xticks(timestamps[:: len(timestamps) // 24]) # Select 10 evenly spaced ticks
|
||||||
|
ax2.set_xticklabels([f"{int(h)}" for h in hours_since_start[:: len(timestamps) // 24]])
|
||||||
|
if x2label:
|
||||||
|
ax2.set_xlabel(x2label)
|
||||||
|
|
||||||
|
# Ensure ax1 and ax2 are aligned
|
||||||
|
# assert ax1.get_xlim() == ax2.get_xlim(), "ax1 and ax2 are not aligned"
|
||||||
|
|
||||||
|
self.add_chart_to_group(chart) # Add chart function to current group
|
||||||
|
|
||||||
def create_line_chart(
|
def create_line_chart(
|
||||||
self,
|
self,
|
||||||
start_hour: Optional[int],
|
start_hour: Optional[int],
|
||||||
@ -143,6 +241,7 @@ class VisualizationReport(ConfigMixin):
|
|||||||
line_styles[idx] if line_styles and idx < len(line_styles) else "-"
|
line_styles[idx] if line_styles and idx < len(line_styles) else "-"
|
||||||
) # Line style
|
) # Line style
|
||||||
plt.plot(x, y_data, label=label, marker=marker, linestyle=line_style) # Plot line
|
plt.plot(x, y_data, label=label, marker=marker, linestyle=line_style) # Plot line
|
||||||
|
|
||||||
plt.title(title) # Set title
|
plt.title(title) # Set title
|
||||||
plt.xlabel(xlabel) # Set x-axis label
|
plt.xlabel(xlabel) # Set x-axis label
|
||||||
plt.ylabel(ylabel) # Set y-axis label
|
plt.ylabel(ylabel) # Set y-axis label
|
||||||
@ -314,45 +413,53 @@ def prepare_visualize(
|
|||||||
start_hour: Optional[int] = 0,
|
start_hour: Optional[int] = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
report = VisualizationReport(filename)
|
report = VisualizationReport(filename)
|
||||||
|
next_full_hour_date = pendulum.now().start_of("hour").add(hours=1)
|
||||||
# Group 1:
|
# Group 1:
|
||||||
report.create_line_chart(
|
print(parameters.ems.gesamtlast)
|
||||||
None,
|
report.create_line_chart_date(
|
||||||
[parameters.ems.gesamtlast],
|
next_full_hour_date, # start_date
|
||||||
|
[
|
||||||
|
parameters.ems.gesamtlast,
|
||||||
|
],
|
||||||
title="Load Profile",
|
title="Load Profile",
|
||||||
xlabel="Hours",
|
# xlabel="Hours", # not enough space
|
||||||
ylabel="Load (Wh)",
|
ylabel="Load (Wh)",
|
||||||
labels=["Total Load (Wh)"],
|
labels=["Total Load (Wh)"],
|
||||||
markers=["s"],
|
|
||||||
line_styles=["-"],
|
|
||||||
)
|
)
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
None,
|
next_full_hour_date, # start_date
|
||||||
[parameters.ems.pv_prognose_wh],
|
[
|
||||||
|
parameters.ems.pv_prognose_wh,
|
||||||
|
],
|
||||||
title="PV Forecast",
|
title="PV Forecast",
|
||||||
xlabel="Hours",
|
# xlabel="Hours", # not enough space
|
||||||
ylabel="PV Generation (Wh)",
|
ylabel="PV Generation (Wh)",
|
||||||
)
|
)
|
||||||
|
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
None,
|
next_full_hour_date, # start_date
|
||||||
[np.full(len(parameters.ems.gesamtlast), parameters.ems.einspeiseverguetung_euro_pro_wh)],
|
[np.full(len(parameters.ems.gesamtlast), parameters.ems.einspeiseverguetung_euro_pro_wh)],
|
||||||
title="Remuneration",
|
title="Remuneration",
|
||||||
xlabel="Hours",
|
# xlabel="Hours", # not enough space
|
||||||
ylabel="€/Wh",
|
ylabel="€/Wh",
|
||||||
|
x2label=None, # not enough space
|
||||||
)
|
)
|
||||||
if parameters.temperature_forecast:
|
if parameters.temperature_forecast:
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
None,
|
next_full_hour_date, # start_date
|
||||||
[parameters.temperature_forecast],
|
[
|
||||||
|
parameters.temperature_forecast,
|
||||||
|
],
|
||||||
title="Temperature Forecast",
|
title="Temperature Forecast",
|
||||||
xlabel="Hours",
|
# xlabel="Hours", # not enough space
|
||||||
ylabel="°C",
|
ylabel="°C",
|
||||||
|
x2label=None, # not enough space
|
||||||
)
|
)
|
||||||
report.finalize_group()
|
report.finalize_group()
|
||||||
|
|
||||||
# Group 2:
|
# Group 2:
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
start_hour,
|
next_full_hour_date, # start_date
|
||||||
[
|
[
|
||||||
results["result"]["Last_Wh_pro_Stunde"],
|
results["result"]["Last_Wh_pro_Stunde"],
|
||||||
results["result"]["Home_appliance_wh_per_hour"],
|
results["result"]["Home_appliance_wh_per_hour"],
|
||||||
@ -361,7 +468,7 @@ def prepare_visualize(
|
|||||||
results["result"]["Verluste_Pro_Stunde"],
|
results["result"]["Verluste_Pro_Stunde"],
|
||||||
],
|
],
|
||||||
title="Energy Flow per Hour",
|
title="Energy Flow per Hour",
|
||||||
xlabel="Hours",
|
# xlabel="Date", # not enough space
|
||||||
ylabel="Energy (Wh)",
|
ylabel="Energy (Wh)",
|
||||||
labels=[
|
labels=[
|
||||||
"Load (Wh)",
|
"Load (Wh)",
|
||||||
@ -376,11 +483,11 @@ def prepare_visualize(
|
|||||||
report.finalize_group()
|
report.finalize_group()
|
||||||
|
|
||||||
# Group 3:
|
# Group 3:
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
start_hour,
|
next_full_hour_date, # start_date
|
||||||
[results["result"]["akku_soc_pro_stunde"], results["result"]["EAuto_SoC_pro_Stunde"]],
|
[results["result"]["akku_soc_pro_stunde"], results["result"]["EAuto_SoC_pro_Stunde"]],
|
||||||
title="Battery SOC",
|
title="Battery SOC",
|
||||||
xlabel="Hours",
|
# xlabel="Date", # not enough space
|
||||||
ylabel="%",
|
ylabel="%",
|
||||||
labels=[
|
labels=[
|
||||||
"Battery SOC (%)",
|
"Battery SOC (%)",
|
||||||
@ -388,12 +495,13 @@ def prepare_visualize(
|
|||||||
],
|
],
|
||||||
markers=["o", "x"],
|
markers=["o", "x"],
|
||||||
)
|
)
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
None,
|
next_full_hour_date, # start_date
|
||||||
[parameters.ems.strompreis_euro_pro_wh],
|
[parameters.ems.strompreis_euro_pro_wh],
|
||||||
title="Electricity Price",
|
# title="Electricity Price", # not enough space
|
||||||
xlabel="Hours",
|
# xlabel="Date", # not enough space
|
||||||
ylabel="Price (€/Wh)",
|
ylabel="Electricity Price (€/Wh)",
|
||||||
|
x2label=None, # not enough space
|
||||||
)
|
)
|
||||||
|
|
||||||
report.create_bar_chart(
|
report.create_bar_chart(
|
||||||
@ -409,14 +517,14 @@ def prepare_visualize(
|
|||||||
|
|
||||||
# Group 4:
|
# Group 4:
|
||||||
|
|
||||||
report.create_line_chart(
|
report.create_line_chart_date(
|
||||||
start_hour,
|
next_full_hour_date, # start_date
|
||||||
[
|
[
|
||||||
results["result"]["Kosten_Euro_pro_Stunde"],
|
results["result"]["Kosten_Euro_pro_Stunde"],
|
||||||
results["result"]["Einnahmen_Euro_pro_Stunde"],
|
results["result"]["Einnahmen_Euro_pro_Stunde"],
|
||||||
],
|
],
|
||||||
title="Financial Balance per Hour",
|
title="Financial Balance per Hour",
|
||||||
xlabel="Hours",
|
# xlabel="Date", # not enough space
|
||||||
ylabel="Euro",
|
ylabel="Euro",
|
||||||
labels=["Costs", "Revenue"],
|
labels=["Costs", "Revenue"],
|
||||||
)
|
)
|
||||||
@ -511,7 +619,7 @@ def prepare_visualize(
|
|||||||
|
|
||||||
def generate_example_report(filename: str = "example_report.pdf") -> None:
|
def generate_example_report(filename: str = "example_report.pdf") -> None:
|
||||||
"""Generate example visualization report."""
|
"""Generate example visualization report."""
|
||||||
report = VisualizationReport(filename)
|
report = VisualizationReport(filename, "test")
|
||||||
x_hours = 0 # Define x-axis start values (e.g., hours)
|
x_hours = 0 # Define x-axis start values (e.g., hours)
|
||||||
|
|
||||||
# Group 1: Adding charts to be displayed on the same page
|
# Group 1: Adding charts to be displayed on the same page
|
||||||
@ -617,6 +725,14 @@ def generate_example_report(filename: str = "example_report.pdf") -> None:
|
|||||||
report.add_json_page(json_obj=sample_json, title="Formatted JSON Data", fontsize=10)
|
report.add_json_page(json_obj=sample_json, title="Formatted JSON Data", fontsize=10)
|
||||||
report.finalize_group()
|
report.finalize_group()
|
||||||
|
|
||||||
|
report.create_line_chart_date(
|
||||||
|
pendulum.now().subtract(hours=0),
|
||||||
|
[list(np.random.random(840))],
|
||||||
|
title="test",
|
||||||
|
xlabel="test",
|
||||||
|
ylabel="test",
|
||||||
|
)
|
||||||
|
report.finalize_group()
|
||||||
# Generate the PDF report
|
# Generate the PDF report
|
||||||
report.generate_pdf()
|
report.generate_pdf()
|
||||||
|
|
||||||
|
BIN
tests/testdata/test_example_report.pdf
vendored
BIN
tests/testdata/test_example_report.pdf
vendored
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user