diff --git a/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py b/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py index 854a03a05a..dcd10b35b0 100644 --- a/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py +++ b/src/sas/qtgui/Perspectives/Corfunc/CorfuncPerspective.py @@ -17,7 +17,7 @@ from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Utilities.BackgroundColor import BG_DEFAULT, BG_ERROR from sas.qtgui.Utilities.ExtrapolationSlider import ExtrapolationSlider, SliderPerspective -from sas.qtgui.Utilities.Reports import ReportBase +from sas.qtgui.Utilities.Reports import ReportBase, format_report_parameters from sas.qtgui.Utilities.Reports.reportdata import ReportData from sas.sascalc.corfunc.calculation_data import ( GuinierData, @@ -1046,17 +1046,7 @@ def getReport(self) -> ReportData | None: report.add_data_details(self.data) # Format keys - parameters = self.getState() - fancy_parameters = {} - - for key in parameters: - nice_key = " ".join([s.capitalize() for s in key.split("_")]) - if parameters[key].strip() == '': - fancy_parameters[nice_key] = '-' - else: - fancy_parameters[nice_key] = parameters[key] - - report.add_table_dict(fancy_parameters, ("Parameter", "Value")) + report.add_table_dict(format_report_parameters(self.getState()), ("Parameter", "Value")) report.add_plot(self.q_space_figure) report.add_plot(self.real_space_figure) report.add_plot(self.extraction_figure) diff --git a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py index 0294b7d8c9..a97bedcb7f 100644 --- a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py +++ b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py @@ -7,12 +7,12 @@ import html2text from bumps import options -from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas +from matplotlib.figure import Figure from sasmodels import __version__ as SASMODELS_VERSION +import sas.qtgui.Plotting.PlotHelper as PlotHelper import sas.qtgui.Utilities.GuiUtils as GuiUtils -from sas.qtgui.Plotting.PlotterBase import PlotterBase from sas.qtgui.Utilities.Reports.reportdata import ReportData from sas.system.version import __version__ as SASVIEW_VERSION @@ -80,12 +80,11 @@ def reportHeader(self) -> str: return report - def buildPlotsForReport(self, images: list[PlotterBase]) -> str: + def buildPlotsForReport(self, images: list[Figure]) -> str: """ Convert Matplotlib figure 'fig' into a tag for HTML use using base64 encoding. """ html = FEET_1.format(self.data.name) for fig in images: - canvas = FigureCanvas(fig) png_output = BytesIO() try: dpi = 150 if sys.platform == "darwin" else 75 @@ -98,7 +97,6 @@ def buildPlotsForReport(self, images: list[PlotterBase]) -> str: feet = FEET_3 if sys.platform == "darwin" else FEET_2 html += feet.format(data_to_print) + ELINE png_output.close() - del canvas return html def reportParams(self) -> str: @@ -156,7 +154,7 @@ def getFitSmearing(self) -> str: smear_format = CENTRE.format(f"Smearing Information: {smear_format}") return smear_format - def getResultsPlots(self) -> list[FigureCanvas]: + def getResultsPlots(self) -> list[Figure]: """Gather the plots from the bumps results panel.""" plots = [] if hasattr(self.parent, 'parent') and hasattr(self.parent.parent, 'results_panel'): @@ -199,27 +197,18 @@ def getBatchResults(self) -> str: batch_results_table += '' return batch_results_table - def getImages(self) -> list[PlotterBase]: - """Create MPL figures for the current fit""" - graphs = [] + def getImages(self) -> list[Figure]: + """Create MPL figures for the current fit. + + Uses `PlotHelper.figures_for_plot_ids` to collect any live figures shown + for the dataset, then appends result-panel figures. + """ modelname = self.kernel_module.name if not modelname or self._index is None: return [] - plot_ids = [plot.id for plot in GuiUtils.plotsFromModel(modelname, self._index)] - - # Active plots - import sas.qtgui.Plotting.PlotHelper as PlotHelper - shown_plot_names = PlotHelper.currentPlotIds() - - # current_plots = list of graph names of currently shown plots - # which are related to this dataset - current_plots = [name for name in shown_plot_names if PlotHelper.plotById(name).data[0].id in plot_ids] - - for name in current_plots: - # get the plotter object first - plotter = PlotHelper.plotById(name) - graphs.append(plotter.figure) + plot_ids = [plot.id for plot in GuiUtils.plotsFromModel(modelname, self._index)] + graphs = PlotHelper.figures_for_plot_ids(plot_ids) graphs.extend(self.getResultsPlots()) return graphs diff --git a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py index 2efdbc1923..0fcd2c0054 100644 --- a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py +++ b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py @@ -2,10 +2,12 @@ from types import TracebackType import numpy as np +from matplotlib.figure import Figure from PySide6 import QtCore, QtGui, QtWidgets from sasdata.dataloader.data_info import Data1D as LoadData1D +import sas.qtgui.Plotting.PlotHelper as PlotHelper from sas.qtgui.Perspectives.perspective import Perspective from sas.qtgui.Perspectives.SizeDistribution.SizeDistributionLogic import ( SizeDistributionLogic, @@ -24,6 +26,8 @@ ) from sas.qtgui.Plotting.PlotterData import Data1D from sas.qtgui.Utilities import GuiUtils +from sas.qtgui.Utilities.Reports import ReportBase, format_report_parameters +from sas.qtgui.Utilities.Reports.reportdata import ReportData ASPECT_RATIO: float = 1.0 DIAMETER_MIN: float = 10.0 @@ -124,6 +128,11 @@ def isSerializable(self) -> bool: """Tell the caller that this perspective writes its state.""" return True + @property + def supports_reports(self) -> bool: + """Tell the caller that this perspective can generate reports.""" + return True + def closeEvent(self, event: QtGui.QCloseEvent) -> None: """Overwrite QDialog close method to allow for custom widget close.""" # Close report widgets before closing/minimizing main widget @@ -503,6 +512,55 @@ def getState(self) -> dict[str, str | bool]: "scale_low_q": self.txtScaleLowQ.text(), } + def getReport(self) -> ReportData | None: + """Build a report for the current Size Distribution analysis.""" + if self.logic.data is None: + return None + + report = ReportBase("Size Distribution") + report.add_data_details(self.logic.data) + + report.add_table_dict(format_report_parameters(self.getState()), ("Parameter", "Value")) + + result_details: dict[str, str] = {} + if self.txtChiSq.text().strip(): + result_details["Chi Squared"] = self.txtChiSq.text().strip() + if self.txtVolume.text().strip(): + result_details["Volume"] = self.txtVolume.text().strip() + if self.txtDiameterMean.text().strip(): + result_details["Diameter Mean"] = self.txtDiameterMean.text().strip() + if self.txtDiameterMedian.text().strip(): + result_details["Diameter Median"] = self.txtDiameterMedian.text().strip() + if self.txtDiameterMode.text().strip(): + result_details["Diameter Mode"] = self.txtDiameterMode.text().strip() + + if result_details: + report.add_table_dict(result_details, ("Result", "Value")) + + images = self.getImages() + # Add existing figures to the report (use their axes title if present) + for fig in images: + title = fig.axes[0].get_title() if fig.axes else None + report.add_plot(fig, figure_title=title) + + return report.report_data + + def getImages(self) -> list[Figure]: + """Return live Matplotlib figures for the current model item. + + This collects the Data1D/Data2D ids for `self._model_item` and then + delegates to `PlotHelper.figures_for_plot_ids`. Returns empty list + if no model item or no plots found. + """ + model_item = getattr(self, '_model_item', None) + if not model_item: + return [] + + plot_data = GuiUtils.plotsFromModel("", model_item) + plot_ids = [p.id for p in plot_data] + + return PlotHelper.figures_for_plot_ids(plot_ids) + def removeData(self, data_list: list | None = None) -> None: """Remove the existing data reference from the Size Distribution Perspective.""" if not data_list or self._model_item not in data_list: diff --git a/src/sas/qtgui/Plotting/PlotHelper.py b/src/sas/qtgui/Plotting/PlotHelper.py index abc684ec58..d3b3c41e79 100644 --- a/src/sas/qtgui/Plotting/PlotHelper.py +++ b/src/sas/qtgui/Plotting/PlotHelper.py @@ -6,6 +6,8 @@ import sys import weakref +from matplotlib.figure import Figure + # TODO Refactor to allow typing without circular import #from sas.qtgui.Plotting.PlotterBase import PlotterBase @@ -58,3 +60,32 @@ def idOfPlot(plot): break return plot_id + + +def figures_for_plot_ids(plot_ids: list) -> list[Figure]: + """ + Return a list of Matplotlib `Figure` objects for active plotters whose data ids + are in `plot_ids`. + + Notes: + - `plot_ids` may contain non-string IDs (e.g., integers) and may include `None`. + `None` values are ignored. + - Returns an empty list if no matches are found. + """ + graphs: list[Figure] = [] + # filter out None so comparisons are meaningful + allowed_ids: list = [pid for pid in plot_ids if pid is not None] + if not allowed_ids: + return graphs + + shown_plot_names = currentPlotIds() + + for name in shown_plot_names: + plotter = plotById(name) + data = getattr(plotter, "data", None) + if data and getattr(data[0], "id", None) in allowed_ids: + fig = getattr(plotter, "figure", None) + if fig is not None: + graphs.append(fig) + + return graphs diff --git a/src/sas/qtgui/Utilities/Reports/__init__.py b/src/sas/qtgui/Utilities/Reports/__init__.py index 578c1ee7b1..0da9bbe20f 100644 --- a/src/sas/qtgui/Utilities/Reports/__init__.py +++ b/src/sas/qtgui/Utilities/Reports/__init__.py @@ -1,2 +1,2 @@ from .reportdata import ReportData -from .reports import ReportBase +from .reports import ReportBase, format_report_parameters diff --git a/src/sas/qtgui/Utilities/Reports/reports.py b/src/sas/qtgui/Utilities/Reports/reports.py index 1887563373..5882b03428 100644 --- a/src/sas/qtgui/Utilities/Reports/reports.py +++ b/src/sas/qtgui/Utilities/Reports/reports.py @@ -24,6 +24,18 @@ logger = logging.getLogger(__name__) +def format_report_parameters(parameters: dict[str, Any], empty_value: str = "-") -> dict[str, Any]: + """Return a report-friendly copy of a parameter dictionary.""" + formatted_parameters: dict[str, Any] = {} + for key, value in parameters.items(): + nice_key = " ".join(part.capitalize() for part in key.split("_")) + if isinstance(value, str) and value.strip() == "": + formatted_parameters[nice_key] = empty_value + else: + formatted_parameters[nice_key] = value + return formatted_parameters + + # # Utility classes #