mirror of
				https://github.com/Akkudoktor-EOS/EOS.git
				synced 2025-10-30 22:36:21 +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:
		| @@ -5,19 +5,22 @@ import textwrap | ||||
| from collections.abc import Sequence | ||||
| from typing import Callable, Optional, Union | ||||
|  | ||||
| import matplotlib.dates as mdates | ||||
| import matplotlib.pyplot as plt | ||||
| import numpy as np | ||||
| import pendulum | ||||
| from matplotlib.backends.backend_pdf import PdfPages | ||||
|  | ||||
| from akkudoktoreos.core.coreabc import ConfigMixin | ||||
| from akkudoktoreos.core.logging import get_logger | ||||
| from akkudoktoreos.optimization.genetic import OptimizationParameters | ||||
| from akkudoktoreos.utils.datetimeutil import to_datetime | ||||
|  | ||||
| logger = get_logger(__name__) | ||||
|  | ||||
|  | ||||
| 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 | ||||
|         self.filename = filename | ||||
|         self.groups: list[list[Callable[[], None]]] = []  # Store groups of charts | ||||
| @@ -25,6 +28,8 @@ class VisualizationReport(ConfigMixin): | ||||
|             Callable[[], None] | ||||
|         ] = []  # Store current group of charts being created | ||||
|         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: | ||||
|         """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)) | ||||
|             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 | ||||
|         for idx, chart_func in enumerate(group): | ||||
|             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 | ||||
|         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( | ||||
|         self, | ||||
|         start_hour: Optional[int], | ||||
| @@ -143,6 +241,7 @@ class VisualizationReport(ConfigMixin): | ||||
|                     line_styles[idx] if line_styles and idx < len(line_styles) else "-" | ||||
|                 )  # Line style | ||||
|                 plt.plot(x, y_data, label=label, marker=marker, linestyle=line_style)  # Plot line | ||||
|  | ||||
|             plt.title(title)  # Set title | ||||
|             plt.xlabel(xlabel)  # Set x-axis label | ||||
|             plt.ylabel(ylabel)  # Set y-axis label | ||||
| @@ -314,45 +413,53 @@ def prepare_visualize( | ||||
|     start_hour: Optional[int] = 0, | ||||
| ) -> None: | ||||
|     report = VisualizationReport(filename) | ||||
|     next_full_hour_date = pendulum.now().start_of("hour").add(hours=1) | ||||
|     # Group 1: | ||||
|     report.create_line_chart( | ||||
|         None, | ||||
|         [parameters.ems.gesamtlast], | ||||
|     print(parameters.ems.gesamtlast) | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [ | ||||
|             parameters.ems.gesamtlast, | ||||
|         ], | ||||
|         title="Load Profile", | ||||
|         xlabel="Hours", | ||||
|         # xlabel="Hours", # not enough space | ||||
|         ylabel="Load (Wh)", | ||||
|         labels=["Total Load (Wh)"], | ||||
|         markers=["s"], | ||||
|         line_styles=["-"], | ||||
|     ) | ||||
|     report.create_line_chart( | ||||
|         None, | ||||
|         [parameters.ems.pv_prognose_wh], | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [ | ||||
|             parameters.ems.pv_prognose_wh, | ||||
|         ], | ||||
|         title="PV Forecast", | ||||
|         xlabel="Hours", | ||||
|         # xlabel="Hours", # not enough space | ||||
|         ylabel="PV Generation (Wh)", | ||||
|     ) | ||||
|  | ||||
|     report.create_line_chart( | ||||
|         None, | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [np.full(len(parameters.ems.gesamtlast), parameters.ems.einspeiseverguetung_euro_pro_wh)], | ||||
|         title="Remuneration", | ||||
|         xlabel="Hours", | ||||
|         # xlabel="Hours", # not enough space | ||||
|         ylabel="€/Wh", | ||||
|         x2label=None,  # not enough space | ||||
|     ) | ||||
|     if parameters.temperature_forecast: | ||||
|         report.create_line_chart( | ||||
|             None, | ||||
|             [parameters.temperature_forecast], | ||||
|         report.create_line_chart_date( | ||||
|             next_full_hour_date,  # start_date | ||||
|             [ | ||||
|                 parameters.temperature_forecast, | ||||
|             ], | ||||
|             title="Temperature Forecast", | ||||
|             xlabel="Hours", | ||||
|             # xlabel="Hours", # not enough space | ||||
|             ylabel="°C", | ||||
|             x2label=None,  # not enough space | ||||
|         ) | ||||
|     report.finalize_group() | ||||
|  | ||||
|     # Group 2: | ||||
|     report.create_line_chart( | ||||
|         start_hour, | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [ | ||||
|             results["result"]["Last_Wh_pro_Stunde"], | ||||
|             results["result"]["Home_appliance_wh_per_hour"], | ||||
| @@ -361,7 +468,7 @@ def prepare_visualize( | ||||
|             results["result"]["Verluste_Pro_Stunde"], | ||||
|         ], | ||||
|         title="Energy Flow per Hour", | ||||
|         xlabel="Hours", | ||||
|         # xlabel="Date", # not enough space | ||||
|         ylabel="Energy (Wh)", | ||||
|         labels=[ | ||||
|             "Load (Wh)", | ||||
| @@ -376,11 +483,11 @@ def prepare_visualize( | ||||
|     report.finalize_group() | ||||
|  | ||||
|     # Group 3: | ||||
|     report.create_line_chart( | ||||
|         start_hour, | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [results["result"]["akku_soc_pro_stunde"], results["result"]["EAuto_SoC_pro_Stunde"]], | ||||
|         title="Battery SOC", | ||||
|         xlabel="Hours", | ||||
|         # xlabel="Date", # not enough space | ||||
|         ylabel="%", | ||||
|         labels=[ | ||||
|             "Battery SOC (%)", | ||||
| @@ -388,12 +495,13 @@ def prepare_visualize( | ||||
|         ], | ||||
|         markers=["o", "x"], | ||||
|     ) | ||||
|     report.create_line_chart( | ||||
|         None, | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [parameters.ems.strompreis_euro_pro_wh], | ||||
|         title="Electricity Price", | ||||
|         xlabel="Hours", | ||||
|         ylabel="Price (€/Wh)", | ||||
|         # title="Electricity Price", # not enough space | ||||
|         # xlabel="Date", # not enough space | ||||
|         ylabel="Electricity Price (€/Wh)", | ||||
|         x2label=None,  # not enough space | ||||
|     ) | ||||
|  | ||||
|     report.create_bar_chart( | ||||
| @@ -409,14 +517,14 @@ def prepare_visualize( | ||||
|  | ||||
|     # Group 4: | ||||
|  | ||||
|     report.create_line_chart( | ||||
|         start_hour, | ||||
|     report.create_line_chart_date( | ||||
|         next_full_hour_date,  # start_date | ||||
|         [ | ||||
|             results["result"]["Kosten_Euro_pro_Stunde"], | ||||
|             results["result"]["Einnahmen_Euro_pro_Stunde"], | ||||
|         ], | ||||
|         title="Financial Balance per Hour", | ||||
|         xlabel="Hours", | ||||
|         # xlabel="Date", # not enough space | ||||
|         ylabel="Euro", | ||||
|         labels=["Costs", "Revenue"], | ||||
|     ) | ||||
| @@ -511,7 +619,7 @@ def prepare_visualize( | ||||
|  | ||||
| def generate_example_report(filename: str = "example_report.pdf") -> None: | ||||
|     """Generate example visualization report.""" | ||||
|     report = VisualizationReport(filename) | ||||
|     report = VisualizationReport(filename, "test") | ||||
|     x_hours = 0  # Define x-axis start values (e.g., hours) | ||||
|  | ||||
|     # 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.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 | ||||
|     report.generate_pdf() | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user