From d18c5670a4f449858c4a4d495b479fc3ed4f1cbd Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Tue, 20 Aug 2024 15:29:55 +0200 Subject: [PATCH 01/11] GuiManager.py loads TabbedPlotWidget alongside calculators and other hidden widgets --- src/sas/qtgui/MainWindow/GuiManager.py | 4 ++++ src/sas/qtgui/Plotting/TabbedPlotWidget.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/sas/qtgui/Plotting/TabbedPlotWidget.py diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 6e445b56cd..dd1be45804 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -73,6 +73,8 @@ from sas.qtgui.Utilities.FileConverter import FileConverterWidget from sas.qtgui.Utilities.WhatsNew.WhatsNew import WhatsNew +from sas.qtgui.Plotting.TabbedPlotWidget import TabbedPlotWidget + import sas from sas import config from sas.system import web @@ -212,6 +214,8 @@ def addWidgets(self): self.WhatsNew = WhatsNew(self) self.regenProgress = DocRegenProgress(self) + self.tabbedPlotWidget = TabbedPlotWidget(self) + def loadAllPerspectives(self): """ Load all the perspectives""" # Close any existing perspectives to prevent multiple open instances diff --git a/src/sas/qtgui/Plotting/TabbedPlotWidget.py b/src/sas/qtgui/Plotting/TabbedPlotWidget.py new file mode 100644 index 0000000000..6ec0409004 --- /dev/null +++ b/src/sas/qtgui/Plotting/TabbedPlotWidget.py @@ -0,0 +1,14 @@ +from PySide6 import QtWidgets + +class TabbedPlotWidget(QtWidgets.QTabWidget): + """ + Central plot widget that holds tabs and subtabs for all existing plots + """ + name = 'TabbedPlotWidget' + def __init__(self, parent=None): + super(TabbedPlotWidget, self).__init__() + + self.manager = parent + self.setObjectName('TabbedPlotWidget') + self.setMinimumSize(500, 500) + self.hide() \ No newline at end of file From 85ea48544ab15415ca30f3709dc753c6b3dacbeb Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Tue, 20 Aug 2024 15:42:59 +0200 Subject: [PATCH 02/11] Set TabbedPlotWidget icon and window name --- src/sas/qtgui/Plotting/TabbedPlotWidget.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/sas/qtgui/Plotting/TabbedPlotWidget.py b/src/sas/qtgui/Plotting/TabbedPlotWidget.py index 6ec0409004..ff15b5a517 100644 --- a/src/sas/qtgui/Plotting/TabbedPlotWidget.py +++ b/src/sas/qtgui/Plotting/TabbedPlotWidget.py @@ -1,14 +1,23 @@ from PySide6 import QtWidgets +from PySide6 import QtCore +from PySide6 import QtGui class TabbedPlotWidget(QtWidgets.QTabWidget): """ Central plot widget that holds tabs and subtabs for all existing plots """ - name = 'TabbedPlotWidget' def __init__(self, parent=None): super(TabbedPlotWidget, self).__init__() self.manager = parent - self.setObjectName('TabbedPlotWidget') + + self._set_icon() + self.setWindowTitle('TabbedPlotWidget') + self.setMinimumSize(500, 500) - self.hide() \ No newline at end of file + self.show() + + def _set_icon(self): + icon = QtGui.QIcon() + icon.addFile(u":/res/ball.ico", QtCore.QSize(), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.setWindowIcon(icon) From addfd88875f2088236fa120352cd88b877869b0a Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Tue, 20 Aug 2024 17:40:04 +0200 Subject: [PATCH 03/11] Activating the tabbedplotwidget upon plotting something else --- src/sas/qtgui/MainWindow/DataExplorer.py | 2 ++ src/sas/qtgui/Plotting/TabbedPlotWidget.py | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 896acb4842..5a2a8faf0b 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1158,6 +1158,8 @@ def displayData(self, data_list, id=None): if new_plots: self.plotData(new_plots) + self.parent.tabbedPlotWidget.show_or_activate() + def isPlotShown(self, plot): """ Checks currently shown plots and returns true if match diff --git a/src/sas/qtgui/Plotting/TabbedPlotWidget.py b/src/sas/qtgui/Plotting/TabbedPlotWidget.py index ff15b5a517..65af29d860 100644 --- a/src/sas/qtgui/Plotting/TabbedPlotWidget.py +++ b/src/sas/qtgui/Plotting/TabbedPlotWidget.py @@ -15,9 +15,19 @@ def __init__(self, parent=None): self.setWindowTitle('TabbedPlotWidget') self.setMinimumSize(500, 500) - self.show() + self.hide() def _set_icon(self): icon = QtGui.QIcon() icon.addFile(u":/res/ball.ico", QtCore.QSize(), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon) + + def show_or_activate(self): + """ + Shows the widget itself, if it is hidden. Activates is, if already shown. + """ + if self.isVisible(): + self.activateWindow() + else: + self.show() + self.activateWindow() From 4560e3781dfda41c63ac4fe5f0e790b561edc29c Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Wed, 11 Sep 2024 17:00:12 +0200 Subject: [PATCH 04/11] Initial commit with the TabbedPlotWidget integration changes --- src/sas/qtgui/MainWindow/DataExplorer.py | 1 + .../Perspectives/Fitting/FittingWidget.py | 9 +- src/sas/qtgui/Plotting/Plotter.py | 1 + src/sas/qtgui/Plotting/SubTabs.py | 130 ++++++++++++++++++ src/sas/qtgui/Plotting/TabbedPlotWidget.py | 71 +++++++++- 5 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 src/sas/qtgui/Plotting/SubTabs.py diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 5a2a8faf0b..ffe48f641a 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1208,6 +1208,7 @@ def plotData(self, plots, transform=True): new_plot.plot(plot_set, transform=transform) # active_plots may contain multiple charts self.active_plots[plot_set.name] = new_plot + print("from DataExplorer.plotData: self.active_plots", self.active_plots) elif isinstance(plot_set, Data2D): self.addDataPlot2D(plot_set, item) else: diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index a18088347e..b2d60aa5f0 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -125,7 +125,7 @@ def __init__(self, parent=None, data=None, tab_id=1): super(FittingWidget, self).__init__() # Necessary globals - self.parent = parent + self.parent = parent self.process = None # Default empty value # Which tab is this widget displayed in? @@ -2449,8 +2449,10 @@ def onPlot(self): self.cmdPlot.setText("Compute/Plot") # Force data recalculation so existing charts are updated if not self.data_is_loaded: + print("showTheoryPlot from FittingWidget") self.showTheoryPlot() else: + print("showPlot from FittingWidget") self.showPlot() # This is an important processEvent. # This allows charts to be properly updated in order @@ -2507,6 +2509,11 @@ def _requestPlots(self, item_name, item_model): """ Emits plotRequestedSignal for all plots found in the given model under the provided item name. """ + # send this information to the TabbedPlotWidget so that it can unpack and show the plots as well + print("send to tabbedPlotWidget") + self.parent.tabbedPlotWidget.add_tab(item_name, item_model, self.tab_id) + + print("_requestPlots from FittingWidget") fitpage_name = self.kernel_module.name plots = GuiUtils.plotsFromDisplayName(item_name, item_model) # Has the fitted data been shown? diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index 8d29f5de62..acd930c1f6 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -238,6 +238,7 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T ax.axhline(y=-1, color='gray', linestyle='--') # Update the list of data sets (plots) in chart self.plot_dict[data.name] = data + print("from Plotter: self.plot_dict:", self.plot_dict) self.plot_lines[data.name] = line diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py new file mode 100644 index 0000000000..45457db29f --- /dev/null +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -0,0 +1,130 @@ +from PySide6 import QtWidgets +from PySide6 import QtCore + +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg +from matplotlib.figure import Figure +import matplotlib.pyplot as plt + +from sas.qtgui.Utilities import GuiUtils + +class SubTabs(QtWidgets.QTabWidget): + """ + This class is for keeping all the subtabs for Plots that are displayed in these. One SubTabs item should + always be associated with one QStandardItem plot_item + """ + def __init__(self, parent, plots): + super().__init__(parent=parent) + # keep track of the parent to access the index of the fitpage? + self.parent = parent + self.counter = 1 + + # The idea is: To use the existing infrastructure of the Plotter but not create multiple instances of them + # just to use the plot() function, that the number of Axes that is needed is already created here and + self.ax = None + + self.add_subtab(plots) + + def add_subtab(self, plots): + self.figure = Figure(figsize=(5, 5)) + + + print("from SubTabs: add_subtab plots", plots) + print("from SubTabs: add_subtab len(plots)", len(plots)) + # filling the slots for the plots temporary to try out the functionalities of the dock container and the + # clickable canvas + subplot_count = len(plots) + if subplot_count == 1: + self.ax = self.figure.subplots(subplot_count) + # putting the axes object in a list so that the access can be generic for both cases with multiple + # subplots and without + self.ax = [self.ax] + else: + # for multiple subplots: decide on the ratios for the bigger, central plot and the smaller, side plots + # region for the big central plot in gridspec + gridspec = self.figure.add_gridspec(ncols=2, width_ratios=[3, 1]) + # region for the small side plots in sub_gridspec + sub_gridspec = gridspec[1].subgridspec(ncols=1, nrows=subplot_count-1) + + ax = [self.figure.add_subplot(gridspec[0])] + # add small plots to axes list, so it can be accessed that way + for idx in range(subplot_count - 1): + ax.append(self.figure.add_subplot(sub_gridspec[idx])) + + i = 0 + for item, plot in plots.items(): + # [item, plot] + # data = GuiUtils.dataFromItem(item) + # ax[i].plot(data.x, data.y) + print("from SubTabs in the plotting for loop: plotting") + + + i += 1 + + self.addTab(DockContainer(self.figure), str(self.counter)) + + self.counter += 1 + + +class DockContainer(QtWidgets.QMainWindow): + def __init__(self, figure): + super().__init__() + # TODO: identifier needs to be added to the objectname string -- + # otherwise changing the stylesheet could potentially change the stylesheets of all existing dockcontainers + self.setObjectName("DockContainer") + + # add the dockable widget and set the widget where the canvas will be in and + # the plots will be painted on afterwards + dock_widget = QtWidgets.QDockWidget() + dock_widget.setWidget(CanvasWidget(figure)) + + # connect graying out when docked out method + dock_widget.topLevelChanged.connect(lambda x: self.grayOutOnDock(self, dock_widget)) + + self.addDockWidget(QtCore.Qt.DockWidgetArea.TopDockWidgetArea, dock_widget) + + def grayOutOnDock(self, dock_container: QtWidgets.QMainWindow, dock_widget: QtWidgets.QDockWidget): + """ + Function that is connected to the topLevelChanged slot of the dock widget that lives in one subtab. When the + dock is floating, the area where the dock widget was before, is grayed out. When it is docked in again, + the state is reverted. + """ + name = dock_container.objectName() + if dock_widget.isFloating(): + dock_container.setStyleSheet("QMainWindow#" + name + " { background-color: gray }") + else: + dock_container.setStyleSheet("QMainWindow#" + name + " { background-color: white }") + +class CanvasWidget(QtWidgets.QWidget): + def __init__(self, figure): + super().__init__() + + layout = QtWidgets.QVBoxLayout() + + canvas = ClickableCanvas(figure) + + layout.addWidget(canvas) + + self.setLayout(layout) + +class ClickableCanvas(FigureCanvasQTAgg): + """ + This class provides an extension of the normal Qt Figure Canvas, so that clicks on subplots of a figure can be + processed to switch the plot position. Example: if there are 3 plots in a figure 1,2,3 and plot 3 is clicked, + the clicked plot will always change its position with the plot 1. + """ + def __init__(self, figure): + super().__init__(figure) + self.mpl_connect("button_press_event", self.onclick) + self.big = 0 + + def onclick(self, event): + big = self.big + if event.inaxes: + axs = self.figure.get_axes() + for index, ax in enumerate(axs): + if (index != big) and (ax == event.inaxes): + temp = axs[big].get_position() + axs[big].set_position(axs[index].get_position()) + axs[index].set_position(temp) + self.big = index + self.draw() \ No newline at end of file diff --git a/src/sas/qtgui/Plotting/TabbedPlotWidget.py b/src/sas/qtgui/Plotting/TabbedPlotWidget.py index 65af29d860..e24a510355 100644 --- a/src/sas/qtgui/Plotting/TabbedPlotWidget.py +++ b/src/sas/qtgui/Plotting/TabbedPlotWidget.py @@ -2,6 +2,10 @@ from PySide6 import QtCore from PySide6 import QtGui +from sas.qtgui.Plotting.SubTabs import SubTabs + +from sas.qtgui.Utilities import GuiUtils + class TabbedPlotWidget(QtWidgets.QTabWidget): """ Central plot widget that holds tabs and subtabs for all existing plots @@ -9,8 +13,12 @@ class TabbedPlotWidget(QtWidgets.QTabWidget): def __init__(self, parent=None): super(TabbedPlotWidget, self).__init__() + # the manager/parent of this class is the GuiManager self.manager = parent + # use this dictionary to keep track of the tab that the plots of a certain fitpage are saved in + self.tab_fitpage_dict = {} + self._set_icon() self.setWindowTitle('TabbedPlotWidget') @@ -18,16 +26,77 @@ def __init__(self, parent=None): self.hide() def _set_icon(self): + """ + Set the icon of the window + """ icon = QtGui.QIcon() icon.addFile(u":/res/ball.ico", QtCore.QSize(), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.setWindowIcon(icon) def show_or_activate(self): """ - Shows the widget itself, if it is hidden. Activates is, if already shown. + Shows the widget itself, if it is hidden. Activates it, if already shown. """ if self.isVisible(): self.activateWindow() else: self.show() self.activateWindow() + + def add_tab_to_dict(self, tab_id: int) -> int: + """ + This method handles the bookkeeping for the existing tabs and there respective fitpages. + Only adds the tab_id to the dict, if it does not already exist in there. + Returns the tab index of the existing or newly added tab. + """ + if tab_id not in self.tab_fitpage_dict.keys(): + self.tab_fitpage_dict[tab_id] = self.count() + + return self.tab_fitpage_dict[tab_id] + + def tab_exists(self, tab_id: int) -> bool: + """ + Check if a tab for the given tab_id already exists. + """ + if tab_id in self.tab_fitpage_dict.keys(): + return True + else: + return False + + def add_tab(self, item_name, item_model, tab_id: int): + """ + The idea is to add only one tab for all the plots that are associated with the plot_item QStandardItem + """ + + plots = GuiUtils.plotsFromDisplayName(item_name, item_model) + + current_tab_index = self.add_tab_to_dict(tab_id) + + if not self.tab_exists(tab_id): + self.addTab(SubTabs(self, plots), f"Fitpage {tab_id}") + else: + self.removeTab(current_tab_index) + + self.insertTab(current_tab_index, SubTabs(self, plots), f"Fitpage {tab_id}") + + self.setCurrentIndex(current_tab_index) + + + print("item_name", item_name) + print("item_model", item_model) + print("tab_id", tab_id) + + for i, _ in enumerate(plots.items()): + item, plot = _ + print("i", i) + print("forloop item", item) + # print("forloop plot", plot) # gives the long information about the File, Title, Instrument stuff etc. + i += 1 + + def get_subtab_by_tab_id(self, tab_id: int): + if tab_id in self.tab_fitpage_dict.keys(): + return self.widget(self.tab_fitpage_dict[tab_id]) + else: + return None + + From 3b7b57d6c1605dfcf2b4f3698840ae9bb7952e9f Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Wed, 11 Sep 2024 17:27:43 +0200 Subject: [PATCH 05/11] additional documentation for SubTabs.py --- src/sas/qtgui/Plotting/SubTabs.py | 37 +++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index 45457db29f..eeb6b28969 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -18,13 +18,38 @@ def __init__(self, parent, plots): self.parent = parent self.counter = 1 - # The idea is: To use the existing infrastructure of the Plotter but not create multiple instances of them - # just to use the plot() function, that the number of Axes that is needed is already created here and + # The idea is: I want to use the Axes that are created by the Plotter.plot function and copy them over to the + # TabbedPlotWidget. But since matplotlib does not allow copying of Axes between figures straight away, the Axes + # in the TabbedPlotWidget need to be created straight away and then given to the Plotter so that it can + # populate these Axes with the same Data, Labels, Titles that the QWidgets for every single plot were + # populated with. self.ax = None self.add_subtab(plots) def add_subtab(self, plots): + """ + Function to add a sub tab with the desired functionality to instances of this class (which are: docking, + multiple subplots, interactive subplots for clicking) + The widget that the QTabWidget that is extended by this class looks like this: + QTabWidget stores a Widget in one of its tabs. + This Widget is a QMainWindow (the DockContainer) that can function as a container for the docking function, + so that the window that will have the plot can be docked out, moved around and docked in again. + The QMainWindow (DockContainer) is filled with a DockWidget, which will be able to dock in and out itself. + The content of this DockWidget is a CanvasWidget(QWidget) with a stored layout, where the further widgets can + be added into. + The layout of the CanvasWidget is populated with the canvas that the matplotlib figure with the final number + of subplots will be in. The matplotlib navigation toolbar can also be added to this layout later. + The canvas added to the layout of the CanvasWidget is the ClickableCanvas and extends the FigureCanvasQtAgg + in a way that subplots from the figure of this FigureCanvas can be clicked and will change their position + with the first (big) subplot + + As a flowchart (?): QTabWidget->QMainWindow->QDockWidget->QWidget->QLayout of former QWidget->FigureCanvasQtAgg + """ + + # The idea behind creating the figure here already is to feed it to the creation of the canvas right away, + # because otherwise it can be quite tricky to navigate through all the layers in between to add the figure + # or manipulate all the axes for example self.figure = Figure(figsize=(5, 5)) @@ -66,10 +91,15 @@ def add_subtab(self, plots): class DockContainer(QtWidgets.QMainWindow): + """ + Container for docking purposes. Carries a + """ def __init__(self, figure): super().__init__() # TODO: identifier needs to be added to the objectname string -- # otherwise changing the stylesheet could potentially change the stylesheets of all existing dockcontainers + # a combination of the tab_id from the TabbedPlotWidget and the subtab index of the DockContainer would be a + # sensible approach self.setObjectName("DockContainer") # add the dockable widget and set the widget where the canvas will be in and @@ -95,6 +125,9 @@ def grayOutOnDock(self, dock_container: QtWidgets.QMainWindow, dock_widget: QtWi dock_container.setStyleSheet("QMainWindow#" + name + " { background-color: white }") class CanvasWidget(QtWidgets.QWidget): + """ + QWidget that can be added into the DockWidget in the MainWindow. The layout of this carries the + """ def __init__(self, figure): super().__init__() From 71eb2d976df79d0fff5b7d8a4ae0604a213a5d03 Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Thu, 12 Sep 2024 10:02:05 +0200 Subject: [PATCH 06/11] delete a couple of print statements --- src/sas/qtgui/MainWindow/DataExplorer.py | 7 ++++++- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 2 -- src/sas/qtgui/Plotting/SubTabs.py | 5 ----- src/sas/qtgui/Plotting/TabbedPlotWidget.py | 11 ----------- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index ffe48f641a..d32bd50c90 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1203,18 +1203,23 @@ def plotData(self, plots, transform=True): for item, plot_set in plots: if isinstance(plot_set, Data1D): if 'new_plot' not in locals(): + # Create only one PlotterWidget(QWidget) for a number of datasets that are supposed to be shown in + # the same Widget + print("created PlotterWidget for:", item) new_plot = PlotterWidget(manager=self, parent=self) new_plot.item = item + print("plotted plot for:", item) new_plot.plot(plot_set, transform=transform) # active_plots may contain multiple charts self.active_plots[plot_set.name] = new_plot - print("from DataExplorer.plotData: self.active_plots", self.active_plots) elif isinstance(plot_set, Data2D): self.addDataPlot2D(plot_set, item) else: msg = "Incorrect data type passed to Plotting" raise AttributeError(msg) + print("from DataExplorer.plotData: self.active_plots", self.active_plots) + if 'new_plot' in locals() and \ hasattr(new_plot, 'data') and \ isinstance(new_plot.data[0], Data1D): diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index b2d60aa5f0..33d03f9da7 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2510,10 +2510,8 @@ def _requestPlots(self, item_name, item_model): Emits plotRequestedSignal for all plots found in the given model under the provided item name. """ # send this information to the TabbedPlotWidget so that it can unpack and show the plots as well - print("send to tabbedPlotWidget") self.parent.tabbedPlotWidget.add_tab(item_name, item_model, self.tab_id) - print("_requestPlots from FittingWidget") fitpage_name = self.kernel_module.name plots = GuiUtils.plotsFromDisplayName(item_name, item_model) # Has the fitted data been shown? diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index eeb6b28969..cbb066a55b 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -52,9 +52,6 @@ def add_subtab(self, plots): # or manipulate all the axes for example self.figure = Figure(figsize=(5, 5)) - - print("from SubTabs: add_subtab plots", plots) - print("from SubTabs: add_subtab len(plots)", len(plots)) # filling the slots for the plots temporary to try out the functionalities of the dock container and the # clickable canvas subplot_count = len(plots) @@ -80,8 +77,6 @@ def add_subtab(self, plots): # [item, plot] # data = GuiUtils.dataFromItem(item) # ax[i].plot(data.x, data.y) - print("from SubTabs in the plotting for loop: plotting") - i += 1 diff --git a/src/sas/qtgui/Plotting/TabbedPlotWidget.py b/src/sas/qtgui/Plotting/TabbedPlotWidget.py index e24a510355..cd0782e886 100644 --- a/src/sas/qtgui/Plotting/TabbedPlotWidget.py +++ b/src/sas/qtgui/Plotting/TabbedPlotWidget.py @@ -82,17 +82,6 @@ def add_tab(self, item_name, item_model, tab_id: int): self.setCurrentIndex(current_tab_index) - print("item_name", item_name) - print("item_model", item_model) - print("tab_id", tab_id) - - for i, _ in enumerate(plots.items()): - item, plot = _ - print("i", i) - print("forloop item", item) - # print("forloop plot", plot) # gives the long information about the File, Title, Instrument stuff etc. - i += 1 - def get_subtab_by_tab_id(self, tab_id: int): if tab_id in self.tab_fitpage_dict.keys(): return self.widget(self.tab_fitpage_dict[tab_id]) From 3d095340c36e5a3dd0a560462b5f659ec4dbdad1 Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Thu, 12 Sep 2024 11:13:55 +0200 Subject: [PATCH 07/11] Implement functionality to let subtabs know about the tab_id and its tab index in the tabbedplotwidget --- src/sas/qtgui/Plotting/SubTabs.py | 13 ++++++++++-- src/sas/qtgui/Plotting/TabbedPlotWidget.py | 24 ++++++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index cbb066a55b..cb4c840ee0 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -3,21 +3,24 @@ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg from matplotlib.figure import Figure -import matplotlib.pyplot as plt from sas.qtgui.Utilities import GuiUtils +from sas.qtgui.Plotting import TabbedPlotWidget + class SubTabs(QtWidgets.QTabWidget): """ This class is for keeping all the subtabs for Plots that are displayed in these. One SubTabs item should always be associated with one QStandardItem plot_item """ - def __init__(self, parent, plots): + def __init__(self, parent: TabbedPlotWidget, plots: list): super().__init__(parent=parent) # keep track of the parent to access the index of the fitpage? self.parent = parent self.counter = 1 + self.parent_tab_index = -1 + self.tab_id = -1 # The idea is: I want to use the Axes that are created by the Plotter.plot function and copy them over to the # TabbedPlotWidget. But since matplotlib does not allow copying of Axes between figures straight away, the Axes # in the TabbedPlotWidget need to be created straight away and then given to the Plotter so that it can @@ -27,6 +30,12 @@ def __init__(self, parent, plots): self.add_subtab(plots) + def set_parent_tab_index(self): + self.parent_tab_index = self.parent.indexOf(self) + self.tab_id = self.parent.inv_tab_fitpage_dict[self.parent_tab_index] + print(self.parent_tab_index) + print(self.tab_id) + def add_subtab(self, plots): """ Function to add a sub tab with the desired functionality to instances of this class (which are: docking, diff --git a/src/sas/qtgui/Plotting/TabbedPlotWidget.py b/src/sas/qtgui/Plotting/TabbedPlotWidget.py index cd0782e886..420d887d18 100644 --- a/src/sas/qtgui/Plotting/TabbedPlotWidget.py +++ b/src/sas/qtgui/Plotting/TabbedPlotWidget.py @@ -17,8 +17,15 @@ def __init__(self, parent=None): self.manager = parent # use this dictionary to keep track of the tab that the plots of a certain fitpage are saved in + # works like: {"1": "2"}, where 1 is the tab_id (the number associated to the Fitpage from the FittingWidget) + # and 2 is the index of the corresponding tab to that fitpage in this widget. self.tab_fitpage_dict = {} + # since this correlation should be unambiguous in both directions, this dict can be inverted, so that the + # subtabs of a certain tab can find out, which fitpage they belong to by finding out about the index of their + # parent tab in this widget + self.inv_tab_fitpage_dict = {} + self._set_icon() self.setWindowTitle('TabbedPlotWidget') @@ -52,8 +59,13 @@ def add_tab_to_dict(self, tab_id: int) -> int: if tab_id not in self.tab_fitpage_dict.keys(): self.tab_fitpage_dict[tab_id] = self.count() + self.update_inv_dict(tab_id, self.count()) + return self.tab_fitpage_dict[tab_id] + def update_inv_dict(self, tab_id: int, tab_index: int): + self.inv_tab_fitpage_dict[tab_index] = tab_id + def tab_exists(self, tab_id: int) -> bool: """ Check if a tab for the given tab_id already exists. @@ -72,15 +84,19 @@ def add_tab(self, item_name, item_model, tab_id: int): current_tab_index = self.add_tab_to_dict(tab_id) + new_tab = SubTabs(self, plots) + if not self.tab_exists(tab_id): - self.addTab(SubTabs(self, plots), f"Fitpage {tab_id}") + self.addTab(new_tab, f"Fitpage {tab_id}") + else: self.removeTab(current_tab_index) - - self.insertTab(current_tab_index, SubTabs(self, plots), f"Fitpage {tab_id}") - + self.insertTab(current_tab_index, new_tab, f"Fitpage {tab_id}") self.setCurrentIndex(current_tab_index) + # Set the tab_id and the parent tab index of this SubTabs index so that it knows these values. + new_tab.set_parent_tab_index() + def get_subtab_by_tab_id(self, tab_id: int): if tab_id in self.tab_fitpage_dict.keys(): From 1f82edb10a038aa69578657f5d8f91f153d6cd47 Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Thu, 12 Sep 2024 11:17:43 +0200 Subject: [PATCH 08/11] Additional comments regarding last commit --- src/sas/qtgui/Plotting/SubTabs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index cb4c840ee0..2641a2661a 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -19,8 +19,11 @@ def __init__(self, parent: TabbedPlotWidget, plots: list): self.parent = parent self.counter = 1 + # Set the object parameters after creating this subtab in the tabbedplotwidget. Then this subtab knows + # the tab_id of the parent and the tab index of itself in the parent tab widget. self.parent_tab_index = -1 self.tab_id = -1 + # The idea is: I want to use the Axes that are created by the Plotter.plot function and copy them over to the # TabbedPlotWidget. But since matplotlib does not allow copying of Axes between figures straight away, the Axes # in the TabbedPlotWidget need to be created straight away and then given to the Plotter so that it can From 61ddfc2b0362df5c36b95b24da8331c360a68d6e Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Thu, 12 Sep 2024 14:10:18 +0200 Subject: [PATCH 09/11] running but pretty raw version of drawing something in the tabbedplotwidget at the same time as in the normal seperated qwidgets for plots --- src/sas/qtgui/MainWindow/DataExplorer.py | 17 +- .../Perspectives/Fitting/FittingWidget.py | 5 +- src/sas/qtgui/Plotting/Plotter.py | 326 +++++++++--------- src/sas/qtgui/Plotting/PlotterBase.py | 23 +- src/sas/qtgui/Plotting/SubTabs.py | 9 +- 5 files changed, 191 insertions(+), 189 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index d32bd50c90..bc405acd63 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1087,8 +1087,9 @@ def displayData(self, data_list, id=None): """ Forces display of charts for the given data set """ - # data_list = [QStandardItem, Data1D/Data2D] - plots_to_show = data_list[1:] + # data_list = [QStandardItem, [Axes] Data1D/Data2D] + plots_to_show = data_list[2:] + tpw_axes = data_list[1] plot_item = data_list[0] # plots to show @@ -1109,7 +1110,7 @@ def displayData(self, data_list, id=None): if self.isPlotShown(main_data): self.active_plots[main_data.name].showNormal() else: - self.plotData([(plot_item, main_data)]) + self.plotData([(plot_item, tpw_axes, main_data)]) append = False plot_to_append_to = None @@ -1134,7 +1135,7 @@ def displayData(self, data_list, id=None): continue elif role in stand_alone_types: # Stand-alone plots should always be separate - self.plotData([(plot_item, plot_to_show)]) + self.plotData([(plot_item, tpw_axes, plot_to_show)]) elif append: # Assume all other plots sent together should be on the same chart if a previous plot exists if not plot_to_append_to: @@ -1145,8 +1146,8 @@ def displayData(self, data_list, id=None): # Plots with main data points on the same chart # Get the main data plot unless data is 2D which is plotted earlier if main_data is not None and not isinstance(main_data, Data2D): - new_plots.append((plot_item, main_data)) - new_plots.append((plot_item, plot_to_show)) + new_plots.append((plot_item, tpw_axes, main_data)) + new_plots.append((plot_item, tpw_axes, plot_to_show)) if append: # Append any plots handled in loop before an existing plot was found @@ -1200,13 +1201,13 @@ def plotData(self, plots, transform=True): """ # Call show on requested plots # All same-type charts in one plot - for item, plot_set in plots: + for item, tpw_ax, plot_set in plots: if isinstance(plot_set, Data1D): if 'new_plot' not in locals(): # Create only one PlotterWidget(QWidget) for a number of datasets that are supposed to be shown in # the same Widget print("created PlotterWidget for:", item) - new_plot = PlotterWidget(manager=self, parent=self) + new_plot = PlotterWidget(manager=self, parent=self, tpw_ax=tpw_ax) new_plot.item = item print("plotted plot for:", item) new_plot.plot(plot_set, transform=transform) diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 33d03f9da7..7bb4c88ced 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2512,6 +2512,9 @@ def _requestPlots(self, item_name, item_model): # send this information to the TabbedPlotWidget so that it can unpack and show the plots as well self.parent.tabbedPlotWidget.add_tab(item_name, item_model, self.tab_id) + tpw_axes = self.parent.tabbedPlotWidget.widget(0).ax[0] + print("axes received in FittingWidget:", tpw_axes) + fitpage_name = self.kernel_module.name plots = GuiUtils.plotsFromDisplayName(item_name, item_model) # Has the fitted data been shown? @@ -2520,7 +2523,7 @@ def _requestPlots(self, item_name, item_model): for item, plot in plots.items(): if plot.plot_role != DataRole.ROLE_DATA and fitpage_name in plot.name: data_shown = True - self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id) + self.communicate.plotRequestedSignal.emit([item, tpw_axes, plot], self.tab_id) # return the last data item seen, if nothing was plotted; supposed to be just data) return None if data_shown else item diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index acd930c1f6..06bdf9ad32 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -31,8 +31,8 @@ class PlotterWidget(PlotterBase): """ 1D Plot widget for use with a QDialog """ - def __init__(self, parent=None, manager=None, quickplot=False): - super().__init__(parent, manager=manager, quickplot=quickplot) + def __init__(self, parent=None, manager=None, quickplot=False, tpw_ax=None): + super().__init__(parent, manager=manager, quickplot=quickplot, tpw_ax=tpw_ax) self.parent = parent @@ -141,187 +141,189 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T # Shortcuts ax = self.ax + tpw_ax = self.tpw_ax x = data.view.x y = data.view.y label = data.name # was self._title - # Marker symbol. Passed marker is one of matplotlib.markers characters - # Alternatively, picked up from Data1D as an int index of PlotUtilities.SHAPES dict - if marker is None: - marker = data.symbol - # Try name first - try: - marker = dict(PlotUtilities.SHAPES)[marker] - except KeyError: - marker = list(PlotUtilities.SHAPES.values())[marker] - - assert marker is not None - # Plot name - if data.title: - self.title(title=data.title) - else: - self.title(title=data.name) + for ax in [self.ax, self.tpw_ax]: + # Marker symbol. Passed marker is one of matplotlib.markers characters + # Alternatively, picked up from Data1D as an int index of PlotUtilities.SHAPES dict + if marker is None: + marker = data.symbol + # Try name first + try: + marker = dict(PlotUtilities.SHAPES)[marker] + except KeyError: + marker = list(PlotUtilities.SHAPES.values())[marker] + + assert marker is not None + # Plot name + if data.title: + self.title(title=data.title) + else: + self.title(title=data.name) - # Error marker toggle - if hide_error is None: - hide_error = data.hide_error + # Error marker toggle + if hide_error is None: + hide_error = data.hide_error - # Plot color - if color is None: - color = data.custom_color + # Plot color + if color is None: + color = data.custom_color - # grid on/off, stored on self - ax.grid(self.grid_on) + # grid on/off, stored on self + ax.grid(self.grid_on) - color = PlotUtilities.getValidColor(color) - data.custom_color = color + color = PlotUtilities.getValidColor(color) + data.custom_color = color - markersize = data.markersize + markersize = data.markersize - # Include scaling (log vs. linear) - if version.parse(mpl.__version__) < version.parse("3.3"): - ax.set_xscale(self.xscale, nonposx='clip') if self.xscale != 'linear' else self.ax.set_xscale(self.xscale) - ax.set_yscale(self.yscale, nonposy='clip') if self.yscale != 'linear' else self.ax.set_yscale(self.yscale) - else: - ax.set_xscale(self.xscale, nonpositive='clip') if self.xscale != 'linear' else self.ax.set_xscale(self.xscale) - ax.set_yscale(self.yscale, nonpositive='clip') if self.yscale != 'linear' else self.ax.set_yscale(self.yscale) + # Include scaling (log vs. linear) + if version.parse(mpl.__version__) < version.parse("3.3"): + ax.set_xscale(self.xscale, nonposx='clip') if self.xscale != 'linear' else ax.set_xscale(self.xscale) + ax.set_yscale(self.yscale, nonposy='clip') if self.yscale != 'linear' else ax.set_yscale(self.yscale) + else: + ax.set_xscale(self.xscale, nonpositive='clip') if self.xscale != 'linear' else ax.set_xscale(self.xscale) + ax.set_yscale(self.yscale, nonpositive='clip') if self.yscale != 'linear' else ax.set_yscale(self.yscale) - # Draw non-standard markers - l_width = markersize * 0.4 - if marker == '-' or marker == '--': - line = self.ax.plot(x, y, color=color, lw=l_width, marker='', - linestyle=marker, label=label, zorder=10)[0] + # Draw non-standard markers + l_width = markersize * 0.4 + if marker == '-' or marker == '--': + line = ax.plot(x, y, color=color, lw=l_width, marker='', + linestyle=marker, label=label, zorder=10)[0] - elif marker == 'vline': - y_min = min(y)*9.0/10.0 if min(y) < 0 else 0.0 - line = self.ax.vlines(x=x, ymin=y_min, ymax=y, color=color, - linestyle='-', label=label, lw=l_width, zorder=1) + elif marker == 'vline': + y_min = min(y)*9.0/10.0 if min(y) < 0 else 0.0 + line = ax.vlines(x=x, ymin=y_min, ymax=y, color=color, + linestyle='-', label=label, lw=l_width, zorder=1) - elif marker == 'step': - line = self.ax.step(x, y, color=color, marker='', linestyle='-', - label=label, lw=l_width, zorder=1)[0] + elif marker == 'step': + line = ax.step(x, y, color=color, marker='', linestyle='-', + label=label, lw=l_width, zorder=1)[0] - else: - # plot data with/without errorbars - if hide_error: - line = ax.plot(x, y, marker=marker, color=color, markersize=markersize, - linestyle='', label=label, picker=True) else: - dy = data.view.dy - # Convert tuple (lo,hi) to array [(x-lo),(hi-x)] - if dy is not None and type(dy) == type(()): - dy = np.vstack((y - dy[0], dy[1] - y)).transpose() - - line = ax.errorbar(x, y, - yerr=dy, - xerr=None, - capsize=2, linestyle='', - barsabove=False, - color=color, - marker=marker, - markersize=markersize, - lolims=False, uplims=False, - xlolims=False, xuplims=False, - label=label, - zorder=1, - picker=True) - - # Display horizontal axis if requested - if data.show_yzero: - ax.axhline(color='black', linewidth=1) - - # Display +/- 3 sigma and +/- 1 sigma lines for residual plots - if data.plot_role == DataRole.ROLE_RESIDUAL: - ax.axhline(y=3, color='red', linestyle='-') - ax.axhline(y=-3, color='red', linestyle='-') - ax.axhline(y=1, color='gray', linestyle='--') - ax.axhline(y=-1, color='gray', linestyle='--') - # Update the list of data sets (plots) in chart - self.plot_dict[data.name] = data - print("from Plotter: self.plot_dict:", self.plot_dict) - - self.plot_lines[data.name] = line - - # Now add the legend with some customizations. - if self.showLegend: - max_legend_width = config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH - handles, labels = ax.get_legend_handles_labels() - newhandles = [] - newlabels = [] - for h,l in zip(handles,labels): - if config.FITTING_PLOT_LEGEND_TRUNCATE: - if len(l)> config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH: - half_legend_width = math.floor(max_legend_width/2) - newlabels.append(f'{l[0:half_legend_width-3]} .. {l[-half_legend_width+3:]}') + # plot data with/without errorbars + if hide_error: + line = ax.plot(x, y, marker=marker, color=color, markersize=markersize, + linestyle='', label=label, picker=True) + else: + dy = data.view.dy + # Convert tuple (lo,hi) to array [(x-lo),(hi-x)] + if dy is not None and type(dy) == type(()): + dy = np.vstack((y - dy[0], dy[1] - y)).transpose() + + line = ax.errorbar(x, y, + yerr=dy, + xerr=None, + capsize=2, linestyle='', + barsabove=False, + color=color, + marker=marker, + markersize=markersize, + lolims=False, uplims=False, + xlolims=False, xuplims=False, + label=label, + zorder=1, + picker=True) + + # Display horizontal axis if requested + if data.show_yzero: + ax.axhline(color='black', linewidth=1) + + # Display +/- 3 sigma and +/- 1 sigma lines for residual plots + if data.plot_role == DataRole.ROLE_RESIDUAL: + ax.axhline(y=3, color='red', linestyle='-') + ax.axhline(y=-3, color='red', linestyle='-') + ax.axhline(y=1, color='gray', linestyle='--') + ax.axhline(y=-1, color='gray', linestyle='--') + # Update the list of data sets (plots) in chart + self.plot_dict[data.name] = data + print("from Plotter: self.plot_dict:", self.plot_dict) + + self.plot_lines[data.name] = line + + # Now add the legend with some customizations. + if self.showLegend: + max_legend_width = config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH + handles, labels = ax.get_legend_handles_labels() + newhandles = [] + newlabels = [] + for h,l in zip(handles,labels): + if config.FITTING_PLOT_LEGEND_TRUNCATE: + if len(l)> config.FITTING_PLOT_LEGEND_MAX_LINE_LENGTH: + half_legend_width = math.floor(max_legend_width/2) + newlabels.append(f'{l[0:half_legend_width-3]} .. {l[-half_legend_width+3:]}') + else: + newlabels.append(l) else: - newlabels.append(l) - else: - newlabels.append(textwrap.fill(l,max_legend_width)) - newhandles.append(h) + newlabels.append(textwrap.fill(l,max_legend_width)) + newhandles.append(h) - if config.FITTING_PLOT_FULL_WIDTH_LEGENDS: - self.legend = ax.legend(newhandles,newlabels,loc='best', mode='expand') + if config.FITTING_PLOT_FULL_WIDTH_LEGENDS: + self.legend = ax.legend(newhandles,newlabels,loc='best', mode='expand') + else: + self.legend = ax.legend(newhandles,newlabels,loc='best', shadow=True) + self.legend.set_picker(True) + self.legend.set_visible(self.legendVisible) + + # Current labels for axes + if self.yLabel and not is_fit: + ax.set_ylabel(self.yLabel) + if self.xLabel and not is_fit: + ax.set_xlabel(self.xLabel) + + # define the ranges + if isinstance(self.setRange, SetGraphRange) and self.setRange.rangeModified: + # Assume the range has changed and retain the current and default ranges for future use + modified = self.setRange.rangeModified + default_x_range = self.setRange.defaultXRange + default_y_range = self.setRange.defaultYRange + x_range = self.setRange.xrange() + y_range = self.setRange.yrange() else: - self.legend = ax.legend(newhandles,newlabels,loc='best', shadow=True) - self.legend.set_picker(True) - self.legend.set_visible(self.legendVisible) - - # Current labels for axes - if self.yLabel and not is_fit: - ax.set_ylabel(self.yLabel) - if self.xLabel and not is_fit: - ax.set_xlabel(self.xLabel) - - # define the ranges - if isinstance(self.setRange, SetGraphRange) and self.setRange.rangeModified: - # Assume the range has changed and retain the current and default ranges for future use - modified = self.setRange.rangeModified - default_x_range = self.setRange.defaultXRange - default_y_range = self.setRange.defaultYRange - x_range = self.setRange.xrange() - y_range = self.setRange.yrange() - else: - if isinstance(data, Data1D): - # Get default ranges from data - # factors of .99 and 1.01 provides a small gap so end points not shown right at edge - pad_delta = 0.01 + if isinstance(data, Data1D): + # Get default ranges from data + # factors of .99 and 1.01 provides a small gap so end points not shown right at edge + pad_delta = 0.01 - default_x_range = ((1-pad_delta)*np.min(x), (1+pad_delta)*np.max(x)) + default_x_range = ((1-pad_delta)*np.min(x), (1+pad_delta)*np.max(x)) - # Need to make space for error bars - dy = data.view.dy - if dy is None: - default_y_range = ((1-pad_delta) * np.min(y), (1+pad_delta) * np.max(y)) - else: - default_y_range = ((1-pad_delta)*np.min(np.array(y) - np.array(dy)), - (1+pad_delta)*np.max(np.array(y) + np.array(dy))) + # Need to make space for error bars + dy = data.view.dy + if dy is None: + default_y_range = ((1-pad_delta) * np.min(y), (1+pad_delta) * np.max(y)) + else: + default_y_range = ((1-pad_delta)*np.min(np.array(y) - np.array(dy)), + (1+pad_delta)*np.max(np.array(y) + np.array(dy))) - else: - # Use default ranges given by matplotlib - default_x_range = self.ax.get_xlim() - default_y_range = self.ax.get_ylim() - - x_range = default_x_range - y_range = default_y_range - - modified = False - self.setRange = SetGraphRange(parent=self, x_range=x_range, y_range=y_range) - self.setRange.rangeModified = modified - self.setRange.defaultXRange = default_x_range - self.setRange.defaultYRange = default_y_range - # Go to expected range - self.ax.set_xbound(x_range[0], x_range[1]) - self.ax.set_ybound(y_range[0], y_range[1]) - - # Add q-range sliders - if data.show_q_range_sliders: - # Grab existing slider if it exists - existing_slider = self.sliders.pop(data.name, None) - sliders = QRangeSlider(self, self.ax, data=data) - # New sliders should be visible but existing sliders that were turned off should remain off - if existing_slider is not None and not existing_slider.is_visible: - sliders.toggle() - self.sliders[data.name] = sliders + else: + # Use default ranges given by matplotlib + default_x_range = ax.get_xlim() + default_y_range = ax.get_ylim() + + x_range = default_x_range + y_range = default_y_range + + modified = False + self.setRange = SetGraphRange(parent=self, x_range=x_range, y_range=y_range) + self.setRange.rangeModified = modified + self.setRange.defaultXRange = default_x_range + self.setRange.defaultYRange = default_y_range + # Go to expected range + ax.set_xbound(x_range[0], x_range[1]) + ax.set_ybound(y_range[0], y_range[1]) + + # Add q-range sliders + if data.show_q_range_sliders: + # Grab existing slider if it exists + existing_slider = self.sliders.pop(data.name, None) + sliders = QRangeSlider(self, ax, data=data) + # New sliders should be visible but existing sliders that were turned off should remain off + if existing_slider is not None and not existing_slider.is_visible: + sliders.toggle() + self.sliders[data.name] = sliders # refresh canvas self.canvas.draw_idle() diff --git a/src/sas/qtgui/Plotting/PlotterBase.py b/src/sas/qtgui/Plotting/PlotterBase.py index ca118603b7..0a32a59d0f 100644 --- a/src/sas/qtgui/Plotting/PlotterBase.py +++ b/src/sas/qtgui/Plotting/PlotterBase.py @@ -28,7 +28,7 @@ class PlotterBase(QtWidgets.QWidget): #TODO: Describe what this class is - def __init__(self, parent=None, manager=None, quickplot=False): + def __init__(self, parent=None, manager=None, quickplot=False, tpw_ax=None): super(PlotterBase, self).__init__(parent) # Required for the communicator @@ -102,6 +102,7 @@ def __init__(self, parent=None, manager=None, quickplot=False): # TODO: self.ax will have to be tracked and exposed # to enable subplot specific operations self.ax = self.figure.add_subplot(self.current_plot) + self.tpw_ax = tpw_ax # Remove this, DAMMIT self.axes = [self.ax] @@ -190,10 +191,11 @@ def yscale(self): @yscale.setter def yscale(self, scale='linear'): """ Y-axis scale setter """ - if version.parse(mpl.__version__) < version.parse("3.3"): - self.ax.set_yscale(scale, nonposy='clip') if scale != 'linear' else self.ax.set_yscale(scale) - else: - self.ax.set_yscale(scale, nonpositive='clip') if scale != 'linear' else self.ax.set_yscale(scale) + for placeholder_ax in [self.ax, self.tpw_ax]: + if version.parse(mpl.__version__) < version.parse("3.3"): + placeholder_ax.set_yscale(scale, nonposy='clip') if scale != 'linear' else placeholder_ax.set_yscale(scale) + else: + placeholder_ax.set_yscale(scale, nonpositive='clip') if scale != 'linear' else placeholder_ax.set_yscale(scale) self._yscale = scale @property @@ -204,11 +206,12 @@ def xscale(self): @xscale.setter def xscale(self, scale='linear'): """ X-axis scale setter """ - self.ax.cla() - if version.parse(mpl.__version__) < version.parse("3.3"): - self.ax.set_xscale(scale, nonposx='clip') if scale != 'linear' else self.ax.set_xscale(scale) - else: - self.ax.set_xscale(scale, nonpositive='clip') if scale != 'linear' else self.ax.set_xscale(scale) + for placeholder_ax in [self.ax, self.tpw_ax]: + placeholder_ax.cla() + if version.parse(mpl.__version__) < version.parse("3.3"): + placeholder_ax.set_xscale(scale, nonposx='clip') if scale != 'linear' else placeholder_ax.set_xscale(scale) + else: + placeholder_ax.set_xscale(scale, nonpositive='clip') if scale != 'linear' else placeholder_ax.set_xscale(scale) self._xscale = scale @property diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index 2641a2661a..4960367c5f 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -84,16 +84,9 @@ def add_subtab(self, plots): for idx in range(subplot_count - 1): ax.append(self.figure.add_subplot(sub_gridspec[idx])) - i = 0 - for item, plot in plots.items(): - # [item, plot] - # data = GuiUtils.dataFromItem(item) - # ax[i].plot(data.x, data.y) - - i += 1 + print("axes created in SubTabs:", self.ax[0]) self.addTab(DockContainer(self.figure), str(self.counter)) - self.counter += 1 From aab03e46be5302718afd69986079feb769a1b6ea Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Thu, 12 Sep 2024 15:31:18 +0200 Subject: [PATCH 10/11] Add changes for giving axes from the right subtabs to the plotting for certain axes --- src/sas/qtgui/MainWindow/DataExplorer.py | 10 +++++----- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py | 9 ++++++--- src/sas/qtgui/Plotting/SubTabs.py | 9 +++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index bc405acd63..6c605844c6 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1089,7 +1089,7 @@ def displayData(self, data_list, id=None): """ # data_list = [QStandardItem, [Axes] Data1D/Data2D] plots_to_show = data_list[2:] - tpw_axes = data_list[1] + tpw_ax = data_list[1] plot_item = data_list[0] # plots to show @@ -1110,7 +1110,7 @@ def displayData(self, data_list, id=None): if self.isPlotShown(main_data): self.active_plots[main_data.name].showNormal() else: - self.plotData([(plot_item, tpw_axes, main_data)]) + self.plotData([(plot_item, tpw_ax, main_data)]) append = False plot_to_append_to = None @@ -1135,7 +1135,7 @@ def displayData(self, data_list, id=None): continue elif role in stand_alone_types: # Stand-alone plots should always be separate - self.plotData([(plot_item, tpw_axes, plot_to_show)]) + self.plotData([(plot_item, tpw_ax, plot_to_show)]) elif append: # Assume all other plots sent together should be on the same chart if a previous plot exists if not plot_to_append_to: @@ -1146,8 +1146,8 @@ def displayData(self, data_list, id=None): # Plots with main data points on the same chart # Get the main data plot unless data is 2D which is plotted earlier if main_data is not None and not isinstance(main_data, Data2D): - new_plots.append((plot_item, tpw_axes, main_data)) - new_plots.append((plot_item, tpw_axes, plot_to_show)) + new_plots.append((plot_item, tpw_ax, main_data)) + new_plots.append((plot_item, tpw_ax, plot_to_show)) if append: # Append any plots handled in loop before an existing plot was found diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 7bb4c88ced..4eae1b7938 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2512,7 +2512,9 @@ def _requestPlots(self, item_name, item_model): # send this information to the TabbedPlotWidget so that it can unpack and show the plots as well self.parent.tabbedPlotWidget.add_tab(item_name, item_model, self.tab_id) - tpw_axes = self.parent.tabbedPlotWidget.widget(0).ax[0] + tab_index = self.parent.tabbedPlotWidget.tab_fitpage_dict[self.tab_id] + tpw_axes = self.parent.tabbedPlotWidget.widget(tab_index).ax + print("axes received in FittingWidget:", tpw_axes) fitpage_name = self.kernel_module.name @@ -2520,10 +2522,11 @@ def _requestPlots(self, item_name, item_model): # Has the fitted data been shown? data_shown = False item = None - for item, plot in plots.items(): + for i, item_plot in enumerate(plots.items()): + item, plot = item_plot if plot.plot_role != DataRole.ROLE_DATA and fitpage_name in plot.name: data_shown = True - self.communicate.plotRequestedSignal.emit([item, tpw_axes, plot], self.tab_id) + self.communicate.plotRequestedSignal.emit([item, tpw_axes[i], plot], self.tab_id) # return the last data item seen, if nothing was plotted; supposed to be just data) return None if data_shown else item diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index 4960367c5f..c5c9cee1e2 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -29,7 +29,7 @@ def __init__(self, parent: TabbedPlotWidget, plots: list): # in the TabbedPlotWidget need to be created straight away and then given to the Plotter so that it can # populate these Axes with the same Data, Labels, Titles that the QWidgets for every single plot were # populated with. - self.ax = None + self.ax = [] self.add_subtab(plots) @@ -66,8 +66,9 @@ def add_subtab(self, plots): # filling the slots for the plots temporary to try out the functionalities of the dock container and the # clickable canvas + print("subplot_count len(plots)", len(plots)) subplot_count = len(plots) - if subplot_count == 1: + if subplot_count <= 1: self.ax = self.figure.subplots(subplot_count) # putting the axes object in a list so that the access can be generic for both cases with multiple # subplots and without @@ -79,10 +80,10 @@ def add_subtab(self, plots): # region for the small side plots in sub_gridspec sub_gridspec = gridspec[1].subgridspec(ncols=1, nrows=subplot_count-1) - ax = [self.figure.add_subplot(gridspec[0])] + self.ax = [self.figure.add_subplot(gridspec[0])] # add small plots to axes list, so it can be accessed that way for idx in range(subplot_count - 1): - ax.append(self.figure.add_subplot(sub_gridspec[idx])) + self.ax.append(self.figure.add_subplot(sub_gridspec[idx])) print("axes created in SubTabs:", self.ax[0]) From 2ff082bc16ddc8599e7ccde18f7ea29dc989d01d Mon Sep 17 00:00:00 2001 From: Julius Karliczek Date: Thu, 19 Sep 2024 14:58:15 +0200 Subject: [PATCH 11/11] Multiple plots can be plotted in one window on the first try. Therefore i introduced some way to reorganize the already existing axes in a subtab window. --- src/sas/qtgui/MainWindow/DataExplorer.py | 33 +++++----- .../Perspectives/Fitting/FittingWidget.py | 12 ++-- src/sas/qtgui/Plotting/Plotter.py | 1 - src/sas/qtgui/Plotting/PlotterBase.py | 2 +- src/sas/qtgui/Plotting/SubTabs.py | 66 +++++++++++-------- 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/src/sas/qtgui/MainWindow/DataExplorer.py b/src/sas/qtgui/MainWindow/DataExplorer.py index 6c605844c6..c7258783ea 100644 --- a/src/sas/qtgui/MainWindow/DataExplorer.py +++ b/src/sas/qtgui/MainWindow/DataExplorer.py @@ -1076,20 +1076,19 @@ def displayDataByName(self, name=None, is_data=True, id=None): # Residuals get their own plot if plot.plot_role in [DataRole.ROLE_RESIDUAL, DataRole.ROLE_STAND_ALONE]: plot.yscale = 'linear' - self.plotData([(item, plot)]) + self.plotData([(item, plot)], id) else: new_plots.append((item, plot)) if new_plots: - self.plotData(new_plots) + self.plotData(new_plots, id) - def displayData(self, data_list, id=None): + def displayData(self, data_list, id): """ Forces display of charts for the given data set """ - # data_list = [QStandardItem, [Axes] Data1D/Data2D] - plots_to_show = data_list[2:] - tpw_ax = data_list[1] + # data_list = [QStandardItem, Data1D/Data2D] + plots_to_show = data_list[1:] plot_item = data_list[0] # plots to show @@ -1110,7 +1109,7 @@ def displayData(self, data_list, id=None): if self.isPlotShown(main_data): self.active_plots[main_data.name].showNormal() else: - self.plotData([(plot_item, tpw_ax, main_data)]) + self.plotData([(plot_item, main_data)], id) append = False plot_to_append_to = None @@ -1135,7 +1134,7 @@ def displayData(self, data_list, id=None): continue elif role in stand_alone_types: # Stand-alone plots should always be separate - self.plotData([(plot_item, tpw_ax, plot_to_show)]) + self.plotData([(plot_item, plot_to_show)], id) elif append: # Assume all other plots sent together should be on the same chart if a previous plot exists if not plot_to_append_to: @@ -1146,8 +1145,8 @@ def displayData(self, data_list, id=None): # Plots with main data points on the same chart # Get the main data plot unless data is 2D which is plotted earlier if main_data is not None and not isinstance(main_data, Data2D): - new_plots.append((plot_item, tpw_ax, main_data)) - new_plots.append((plot_item, tpw_ax, plot_to_show)) + new_plots.append((plot_item, main_data)) + new_plots.append((plot_item, plot_to_show)) if append: # Append any plots handled in loop before an existing plot was found @@ -1157,7 +1156,7 @@ def displayData(self, data_list, id=None): new_plots = [] if new_plots: - self.plotData(new_plots) + self.plotData(new_plots, id) self.parent.tabbedPlotWidget.show_or_activate() @@ -1195,18 +1194,22 @@ def addDataPlot2D(self, plot_set, item): # sv.show() # ============================================ - def plotData(self, plots, transform=True): + def plotData(self, plots, tab_id, transform=True): """ Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D) """ + tab_index = self.parent.tabbedPlotWidget.tab_fitpage_dict[tab_id] + print("plotData") # Call show on requested plots # All same-type charts in one plot - for item, tpw_ax, plot_set in plots: + for item, plot_set in plots: if isinstance(plot_set, Data1D): if 'new_plot' not in locals(): # Create only one PlotterWidget(QWidget) for a number of datasets that are supposed to be shown in # the same Widget - print("created PlotterWidget for:", item) + self.parent.tabbedPlotWidget.widget(tab_index).add_more_axes() + tpw_ax = self.parent.tabbedPlotWidget.widget(tab_index).last_axes() + new_plot = PlotterWidget(manager=self, parent=self, tpw_ax=tpw_ax) new_plot.item = item print("plotted plot for:", item) @@ -1219,7 +1222,6 @@ def plotData(self, plots, transform=True): msg = "Incorrect data type passed to Plotting" raise AttributeError(msg) - print("from DataExplorer.plotData: self.active_plots", self.active_plots) if 'new_plot' in locals() and \ hasattr(new_plot, 'data') and \ @@ -1295,6 +1297,7 @@ def appendPlot(self): @staticmethod def appendOrUpdatePlot(self, data, plot): name = data.name + print("append or update plot") if isinstance(plot, Plotter2DWidget) or name in plot.plot_dict.keys(): plot.replacePlot(name, data) else: diff --git a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py index 4eae1b7938..4da8ad57b0 100644 --- a/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py +++ b/src/sas/qtgui/Perspectives/Fitting/FittingWidget.py @@ -2449,10 +2449,8 @@ def onPlot(self): self.cmdPlot.setText("Compute/Plot") # Force data recalculation so existing charts are updated if not self.data_is_loaded: - print("showTheoryPlot from FittingWidget") self.showTheoryPlot() else: - print("showPlot from FittingWidget") self.showPlot() # This is an important processEvent. # This allows charts to be properly updated in order @@ -2512,10 +2510,10 @@ def _requestPlots(self, item_name, item_model): # send this information to the TabbedPlotWidget so that it can unpack and show the plots as well self.parent.tabbedPlotWidget.add_tab(item_name, item_model, self.tab_id) - tab_index = self.parent.tabbedPlotWidget.tab_fitpage_dict[self.tab_id] - tpw_axes = self.parent.tabbedPlotWidget.widget(tab_index).ax + # tab_index = self.parent.tabbedPlotWidget.tab_fitpage_dict[self.tab_id] + # self.parent.tabbedPlotWidget.widget(tab_index).add_more_axes + # tpw_axes = self.parent.tabbedPlotWidget.widget(tab_index).ax - print("axes received in FittingWidget:", tpw_axes) fitpage_name = self.kernel_module.name plots = GuiUtils.plotsFromDisplayName(item_name, item_model) @@ -2526,8 +2524,10 @@ def _requestPlots(self, item_name, item_model): item, plot = item_plot if plot.plot_role != DataRole.ROLE_DATA and fitpage_name in plot.name: data_shown = True - self.communicate.plotRequestedSignal.emit([item, tpw_axes[i], plot], self.tab_id) + self.communicate.plotRequestedSignal.emit([item, plot], self.tab_id) # return the last data item seen, if nothing was plotted; supposed to be just data) + tab_index = self.parent.tabbedPlotWidget.tab_fitpage_dict[self.tab_id] + self.parent.tabbedPlotWidget.widget(tab_index).rearrange_plots() return None if data_shown else item def onOptionsUpdate(self): diff --git a/src/sas/qtgui/Plotting/Plotter.py b/src/sas/qtgui/Plotting/Plotter.py index 06bdf9ad32..98cd7a5aaa 100644 --- a/src/sas/qtgui/Plotting/Plotter.py +++ b/src/sas/qtgui/Plotting/Plotter.py @@ -240,7 +240,6 @@ def plot(self, data=None, color=None, marker=None, hide_error=False, transform=T ax.axhline(y=-1, color='gray', linestyle='--') # Update the list of data sets (plots) in chart self.plot_dict[data.name] = data - print("from Plotter: self.plot_dict:", self.plot_dict) self.plot_lines[data.name] = line diff --git a/src/sas/qtgui/Plotting/PlotterBase.py b/src/sas/qtgui/Plotting/PlotterBase.py index 0a32a59d0f..8d43b403aa 100644 --- a/src/sas/qtgui/Plotting/PlotterBase.py +++ b/src/sas/qtgui/Plotting/PlotterBase.py @@ -36,7 +36,7 @@ def __init__(self, parent=None, manager=None, quickplot=False, tpw_ax=None): self.quickplot = quickplot # Set auto layout so x/y axis captions don't get cut off - rcParams.update({'figure.autolayout': True}) + # rcParams.update({'figure.autolayout': True}) #plt.style.use('ggplot') #plt.style.use('seaborn-darkgrid') diff --git a/src/sas/qtgui/Plotting/SubTabs.py b/src/sas/qtgui/Plotting/SubTabs.py index c5c9cee1e2..6724b088bb 100644 --- a/src/sas/qtgui/Plotting/SubTabs.py +++ b/src/sas/qtgui/Plotting/SubTabs.py @@ -2,7 +2,7 @@ from PySide6 import QtCore from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg -from matplotlib.figure import Figure +import matplotlib.pyplot as plt from sas.qtgui.Utilities import GuiUtils @@ -36,8 +36,6 @@ def __init__(self, parent: TabbedPlotWidget, plots: list): def set_parent_tab_index(self): self.parent_tab_index = self.parent.indexOf(self) self.tab_id = self.parent.inv_tab_fitpage_dict[self.parent_tab_index] - print(self.parent_tab_index) - print(self.tab_id) def add_subtab(self, plots): """ @@ -62,34 +60,48 @@ def add_subtab(self, plots): # The idea behind creating the figure here already is to feed it to the creation of the canvas right away, # because otherwise it can be quite tricky to navigate through all the layers in between to add the figure # or manipulate all the axes for example - self.figure = Figure(figsize=(5, 5)) - - # filling the slots for the plots temporary to try out the functionalities of the dock container and the - # clickable canvas - print("subplot_count len(plots)", len(plots)) - subplot_count = len(plots) - if subplot_count <= 1: - self.ax = self.figure.subplots(subplot_count) - # putting the axes object in a list so that the access can be generic for both cases with multiple - # subplots and without - self.ax = [self.ax] - else: - # for multiple subplots: decide on the ratios for the bigger, central plot and the smaller, side plots - # region for the big central plot in gridspec - gridspec = self.figure.add_gridspec(ncols=2, width_ratios=[3, 1]) - # region for the small side plots in sub_gridspec - sub_gridspec = gridspec[1].subgridspec(ncols=1, nrows=subplot_count-1) - - self.ax = [self.figure.add_subplot(gridspec[0])] - # add small plots to axes list, so it can be accessed that way - for idx in range(subplot_count - 1): - self.ax.append(self.figure.add_subplot(sub_gridspec[idx])) - - print("axes created in SubTabs:", self.ax[0]) + self.figure = plt.figure() self.addTab(DockContainer(self.figure), str(self.counter)) self.counter += 1 + def add_more_axes(self): + """ + Simply adds a new subplot to the figure. + """ + self.figure.add_subplot() + + def last_axes(self): + """ + Get the last axes that was created by add_more_axes + """ + return self.figure.get_axes()[-1] + + def rearrange_plots(self): + """ + This method is called after plotting the results for this tab. It arranges the plots in two columns with the + big plot on the left side and all the other plots on the right side. + """ + print("rearrange_plots") + axes = self.figure.get_axes() + if len(axes) > 2: + pass # nothing to do, just one plot needs to be shown in the middle + elif len(axes) == 2: + # two column gridspec needs to be applied + self.gs = self.figure.add_gridspec(nrows=1, ncols=2) + + # this for loop cannot be replaced by for ax in axes, because the ax is modified in the list :) + for i in range(len(axes)): + axes[i].set_position(self.gs[i].get_position(self.figure)) + else: + # subgridspec needs to be applied + self.gs = self.figure.add_gridspec(nrows=1, ncols=2) + self.sub_gs = self.gs[1].subgridspec(ncols=1, nrows=len(axes)-1) + + axes[0].set_position(self.gs[0].get_position(self.figure)) + for i in range(len(axes)-1): + axes[i+1].set_position(self.gs[i].get_position(self.figure)) + class DockContainer(QtWidgets.QMainWindow): """