174 lines
5.2 KiB
Python
174 lines
5.2 KiB
Python
# Copyright (C) 2026 Antoine ANCEAU
|
|
#
|
|
# This file is part of navaltoolbox.
|
|
|
|
import json
|
|
import logging
|
|
from typing import List, Optional, Dict, Any, TYPE_CHECKING
|
|
|
|
try:
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.figure import Figure
|
|
HAS_MATPLOTLIB = True
|
|
except ImportError:
|
|
HAS_MATPLOTLIB = False
|
|
|
|
if TYPE_CHECKING:
|
|
from . import CriteriaResult
|
|
|
|
|
|
def plot_criteria_result(
|
|
result: "CriteriaResult",
|
|
show: bool = True,
|
|
plot_id: Optional[str] = None,
|
|
save_to: Optional[str] = None
|
|
) -> List["Figure"]:
|
|
"""
|
|
Plot the graphs defined in a CriteriaResult using Matplotlib.
|
|
|
|
Args:
|
|
result: The CriteriaResult object from a script execution.
|
|
show: If True, calls plt.show() after creating figures.
|
|
plot_id: If provided, only plots the graph with this specific ID.
|
|
save_to: Path to save the plot. If multiple plots are generated,
|
|
the plot ID will be appended to the filename.
|
|
|
|
Returns:
|
|
List of created matplotlib Figure objects.
|
|
"""
|
|
if not HAS_MATPLOTLIB:
|
|
raise ImportError(
|
|
"Matplotlib is required for plotting. "
|
|
"Install it with 'pip install matplotlib'."
|
|
)
|
|
|
|
figures = []
|
|
generated_ids = []
|
|
|
|
# Identify relevant criteria for each plot
|
|
criteria_by_plot: Dict[str, List] = {}
|
|
for crit in result.criteria:
|
|
pid = crit.plot_id or "main"
|
|
if pid not in criteria_by_plot:
|
|
criteria_by_plot[pid] = []
|
|
criteria_by_plot[pid].append(crit)
|
|
|
|
# Parse and plot each graph
|
|
for plot_json in result.plots:
|
|
try:
|
|
plot_data = json.loads(plot_json)
|
|
except json.JSONDecodeError:
|
|
logging.error("Failed to decode plot JSON")
|
|
continue
|
|
|
|
pid = plot_data.get("id", "main")
|
|
|
|
# Filter if requested
|
|
if plot_id and pid != plot_id:
|
|
continue
|
|
|
|
fig = _render_single_plot(plot_data, criteria_by_plot.get(pid, []))
|
|
figures.append(fig)
|
|
generated_ids.append(pid)
|
|
|
|
if save_to and figures:
|
|
import os
|
|
base, ext = os.path.splitext(save_to)
|
|
if not ext:
|
|
ext = ".png"
|
|
|
|
if len(figures) == 1:
|
|
figures[0].savefig(save_to)
|
|
logging.info(f"Saved plot to {save_to}")
|
|
else:
|
|
for fig, pid in zip(figures, generated_ids):
|
|
filename = f"{base}_{pid}{ext}"
|
|
fig.savefig(filename)
|
|
logging.info(f"Saved plot {pid} to {filename}")
|
|
|
|
if show and figures:
|
|
plt.show()
|
|
|
|
return figures
|
|
|
|
|
|
def _render_single_plot(plot_data: Dict[str, Any], criteria: List) -> "Figure":
|
|
"""Render a single plot data dictionary to a Matplotlib figure."""
|
|
|
|
# Create figure and axis
|
|
fig, ax = plt.subplots(figsize=(10, 6))
|
|
if fig.canvas.manager:
|
|
fig.canvas.manager.set_window_title(plot_data.get("title", "Plot"))
|
|
|
|
ax.set_title(plot_data.get("title", ""))
|
|
ax.set_xlabel(plot_data.get("x_label", ""))
|
|
ax.set_ylabel(plot_data.get("y_label", ""))
|
|
ax.grid(True, linestyle='--', alpha=0.7)
|
|
|
|
# Render elements
|
|
elements = plot_data.get("elements", [])
|
|
for elem in elements:
|
|
etype = elem.get("type")
|
|
name = elem.get("name", "")
|
|
color = elem.get("color", "blue")
|
|
style = elem.get("style", "-")
|
|
|
|
# Map style strings to matplotlib
|
|
mpl_style = "-"
|
|
if style == "dashed":
|
|
mpl_style = "--"
|
|
elif style == "dotted":
|
|
mpl_style = ":"
|
|
|
|
if etype == "Curve":
|
|
x = elem.get("x", [])
|
|
y = elem.get("y", [])
|
|
ax.plot(x, y, label=name, color=color, linestyle=mpl_style)
|
|
|
|
elif etype == "HorizontalLine":
|
|
y = elem.get("y", 0.0)
|
|
xmin = elem.get("x_min")
|
|
xmax = elem.get("x_max")
|
|
if xmin is not None and xmax is not None:
|
|
ax.hlines(
|
|
y, xmin, xmax, label=name, color=color, linestyle=mpl_style
|
|
)
|
|
else:
|
|
ax.axhline(y, label=name, color=color, linestyle=mpl_style)
|
|
|
|
elif etype == "VerticalLine":
|
|
x = elem.get("x", 0.0)
|
|
ymin = elem.get("y_min")
|
|
ymax = elem.get("y_max")
|
|
if ymin is not None and ymax is not None:
|
|
ax.vlines(
|
|
x, ymin, ymax, label=name, color=color, linestyle=mpl_style
|
|
)
|
|
else:
|
|
ax.axvline(x, label=name, color=color, linestyle=mpl_style)
|
|
|
|
elif etype == "Point":
|
|
x = elem.get("x", 0.0)
|
|
y = elem.get("y", 0.0)
|
|
marker = elem.get("marker", "o")
|
|
# Convert marker name if needed
|
|
if marker == "circle":
|
|
marker = "o"
|
|
ax.plot(
|
|
x, y, marker=marker, color=color, label=name, linestyle='None'
|
|
)
|
|
|
|
elif etype == "FilledArea":
|
|
x = elem.get("x", [])
|
|
y1 = elem.get("y_lower", [])
|
|
y2 = elem.get("y_upper", [])
|
|
alpha = elem.get("alpha", 0.3)
|
|
ax.fill_between(x, y1, y2, color=color, alpha=alpha, label=name)
|
|
|
|
# Add legend
|
|
ax.legend()
|
|
|
|
# Criteria summary could be added here
|
|
# pass
|
|
return fig
|