From 73ae4094249a95dd39b06c7b01e1ff1f1e6770e6 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Tue, 26 May 2026 11:16:41 +0100 Subject: [PATCH 1/6] Refactor reports to use as helper --- .../Perspectives/Corfunc/CorfuncPerspective.py | 14 ++------------ src/sas/qtgui/Utilities/Reports/__init__.py | 2 +- src/sas/qtgui/Utilities/Reports/reports.py | 12 ++++++++++++ 3 files changed, 15 insertions(+), 13 deletions(-) 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/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 # From 38c0d53ba231754e2fed26deabdd93e078e86728 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Tue, 26 May 2026 11:42:37 +0100 Subject: [PATCH 2/6] SD produces PDF report (no plots) --- .../SizeDistributionPerspective.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py index 2efdbc1923..e980cc3c09 100644 --- a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py +++ b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py @@ -24,6 +24,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 +126,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 +510,33 @@ 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")) + + return report.report_data + 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: From 6b0d56b66a59d91e65f22c3cfb962570cae84f3d Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Tue, 26 May 2026 14:43:14 +0100 Subject: [PATCH 3/6] Factor out getting images for report --- .../Perspectives/Fitting/ReportPageLogic.py | 24 +++++++------------ src/sas/qtgui/Plotting/PlotHelper.py | 19 +++++++++++++++ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py index 0294b7d8c9..04ec042f8a 100644 --- a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py +++ b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py @@ -11,6 +11,7 @@ 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 @@ -200,26 +201,17 @@ def getBatchResults(self) -> str: return batch_results_table def getImages(self) -> list[PlotterBase]: - """Create MPL figures for the current fit""" - graphs = [] + """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/Plotting/PlotHelper.py b/src/sas/qtgui/Plotting/PlotHelper.py index abc684ec58..8091ce2870 100644 --- a/src/sas/qtgui/Plotting/PlotHelper.py +++ b/src/sas/qtgui/Plotting/PlotHelper.py @@ -58,3 +58,22 @@ def idOfPlot(plot): break return plot_id + + +def figures_for_plot_ids(plot_ids: list[str]) -> list: + """ + Return a list of Matplotlib `Figure` objects for active plotters whose data ids + are in `plot_ids`. Returns an empty list if no matches are found. + """ + 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 plot_ids: + fig = getattr(plotter, 'figure', None) + if fig is not None: + graphs.append(fig) + + return graphs From 7dee2f50d48f9a7f6a9c3f82442b968dbf1693d9 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Tue, 26 May 2026 14:53:38 +0100 Subject: [PATCH 4/6] Add plots to SD report --- .../SizeDistributionPerspective.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py index e980cc3c09..9e36411ecc 100644 --- a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py +++ b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py @@ -6,6 +6,7 @@ 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, @@ -535,8 +536,32 @@ def getReport(self) -> ReportData | None: 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: + """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 if hasattr(p, 'id')] + if not plot_ids: + return [] + + 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: From 062c0e834fb2d127344308fa4f3423c510fb6bd8 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 27 May 2026 10:22:06 +0100 Subject: [PATCH 5/6] Improve figures_for_plot_ids method --- .../Perspectives/Fitting/ReportPageLogic.py | 6 ++--- .../SizeDistributionPerspective.py | 7 +++--- src/sas/qtgui/Plotting/PlotHelper.py | 24 ++++++++++++++----- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py index 04ec042f8a..3edf6b6459 100644 --- a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py +++ b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py @@ -8,12 +8,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 @@ -81,7 +81,7 @@ 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) @@ -200,7 +200,7 @@ def getBatchResults(self) -> str: batch_results_table += '' return batch_results_table - def getImages(self) -> list[PlotterBase]: + def getImages(self) -> list[Figure]: """Create MPL figures for the current fit. Uses `PlotHelper.figures_for_plot_ids` to collect any live figures shown diff --git a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py index 9e36411ecc..0fcd2c0054 100644 --- a/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py +++ b/src/sas/qtgui/Perspectives/SizeDistribution/SizeDistributionPerspective.py @@ -2,6 +2,7 @@ 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 @@ -544,7 +545,7 @@ def getReport(self) -> ReportData | None: return report.report_data - def getImages(self) -> list: + 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 @@ -556,9 +557,7 @@ def getImages(self) -> list: return [] plot_data = GuiUtils.plotsFromModel("", model_item) - plot_ids = [p.id for p in plot_data if hasattr(p, 'id')] - if not plot_ids: - return [] + plot_ids = [p.id for p in plot_data] return PlotHelper.figures_for_plot_ids(plot_ids) diff --git a/src/sas/qtgui/Plotting/PlotHelper.py b/src/sas/qtgui/Plotting/PlotHelper.py index 8091ce2870..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 @@ -60,19 +62,29 @@ def idOfPlot(plot): return plot_id -def figures_for_plot_ids(plot_ids: list[str]) -> list: +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`. Returns an empty list if no matches are found. + 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 = [] + 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 plot_ids: - fig = getattr(plotter, 'figure', None) + 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) From 10ad45a5081a8cf87cf82eb9f8814288acc2f414 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 27 May 2026 10:39:54 +0100 Subject: [PATCH 6/6] Clear up ReportPageLogic --- src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py index 3edf6b6459..a97bedcb7f 100644 --- a/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py +++ b/src/sas/qtgui/Perspectives/Fitting/ReportPageLogic.py @@ -7,7 +7,6 @@ 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 @@ -86,7 +85,6 @@ def buildPlotsForReport(self, images: list[Figure]) -> str: 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 @@ -99,7 +97,6 @@ def buildPlotsForReport(self, images: list[Figure]) -> 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: @@ -157,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'):