diff --git a/plotting_refactor/DataCollector.py b/plotting_refactor/DataCollector.py new file mode 100644 index 0000000000..90d08fe889 --- /dev/null +++ b/plotting_refactor/DataCollector.py @@ -0,0 +1,118 @@ +import RandomDatasetCreator +from Dataset import Dataset +from PySide6 import QtWidgets + + +class DataCollector: + """ + This class keeps track of all generated datasets. When the update_dataset is called through onCalculate + from MainWindow, either a new dataset can be created or the existing dataset is adjusted with respect to + the currently displayed SpinBox Values from the Fitpage. + """ + def __init__(self): + self._datasets: list[Dataset] = [] + self.datasetcreator = RandomDatasetCreator.DatasetCreator() + + def update_dataset(self, main_window: QtWidgets.QMainWindow, fitpage_index: int, + create_fit: bool, checked_2d: bool): + """ + Search for an existing dataset saved in here. If no dataset for the corresponding fitpage exists: create new + data. + """ + + # search for an existing dataset with the right fitpage_index + existing_dataset_index = -1 + for i, dataset in enumerate(self._datasets): + if dataset.fitpage_index == fitpage_index: + existing_dataset_index = i + + if existing_dataset_index == -1: + # create new dataset in case it does not exist + x_data, y_data, y_fit = self.simulate_data(main_window, create_fit, checked_2d) + plotpage_index = -1 + + dataset = Dataset(fitpage_index, x_data, y_data, y_fit, checked_2d, plotpage_index) + self._datasets.append(dataset) + else: + # update values for existing dataset with respect to the number boxes in the fitpage + x_data, y_data, y_fit = self.simulate_data(main_window, create_fit, checked_2d) + self._datasets[existing_dataset_index].x_data = x_data + self._datasets[existing_dataset_index].y_data = y_data + self._datasets[existing_dataset_index].y_fit = y_fit + self._datasets[existing_dataset_index].is_data_2d = checked_2d + + def simulate_data(self, main_window: QtWidgets.QMainWindow, create_fit: bool, checked_2d: bool): + """ + Collect all information from the FitPage from the MainWindow hat is needed to calculate the test data. + Feed this information to the DatasetCreator and return the values. + """ + combobox_index = main_window.fittingTabs.currentWidget().get_combobox_index() + param_scale = main_window.fittingTabs.currentWidget().doubleSpinBox_scale.value() + param_radius = main_window.fittingTabs.currentWidget().doubleSpinBox_radius.value() + param_height = main_window.fittingTabs.currentWidget().doubleSpinBox_height.value() + + x_data, y_data, y_fit = self.datasetcreator.createRandomDataset(param_scale, param_radius, param_height, + combobox_index, create_fit, checked_2d) + + return x_data, y_data, y_fit + + @property + def datasets(self) -> list: + return self._datasets + + def find_object_by_property(self, obj_list: list, property_name: str, property_value: int): + for obj in obj_list: + if hasattr(obj, property_name) and getattr(obj, property_name) == property_value: + return obj + + return None + + def get_data_by_fp(self, fitpage_index: int) -> Dataset: + """ + Get the dataset for a certain fitpage + """ + return self.find_object_by_property(self._datasets, "fitpage_index", fitpage_index) + + def get_data_by_id(self, data_id: int) -> Dataset: + """ + Get the dataset for certain id + """ + return self.find_object_by_property(self._datasets, "data_id", data_id) + + def get_x_data(self, fitpage_index: int) -> list: + """ + Get x data for certain fitpage index + """ + dataset = self.find_object_by_property(self._datasets, "fitpage_index", fitpage_index) + return dataset.x_data + + def get_y_data(self, fitpage_index: int) -> list: + """ + Get y data for certain fitpage index + """ + dataset = self.find_object_by_property(self._datasets, "fitpage_index", fitpage_index) + return dataset.y_data + + def get_y_fit_data(self, fitpage_index: int) -> list: + """ + Get y fit data for certain fitpage index + """ + dataset = self.find_object_by_property(self._datasets, "fitpage_index", fitpage_index) + return dataset.y_fit + + def get_plotpage_index(self, fitpage_index: int) -> int: + """ + Get the plotpage index for a certain fitpage index: Plotpage index refers to the index of the major tabs in + the plotting widget in which the data is displayed. + """ + dataset = self.find_object_by_property(self._datasets, "fitpage_index", fitpage_index) + return dataset.plotpage_index + + def set_plot_index(self, fitpage_index: int, plot_index: int): + """ + Set the plotpage index for the dataset for a certain fitpage index. Plotpage index refers to the index of the + major tabs in the plotting widget in which the data is displayed. + """ + dataset = self.find_object_by_property(self._datasets, "fitpage_index", fitpage_index) + dataset.plotpage_index = plot_index + diff --git a/plotting_refactor/DataTreeItems.py b/plotting_refactor/DataTreeItems.py new file mode 100644 index 0000000000..b83e70539b --- /dev/null +++ b/plotting_refactor/DataTreeItems.py @@ -0,0 +1,29 @@ +from PySide6.QtWidgets import QTreeWidgetItem + + +class PlotPageItem(QTreeWidgetItem): + def __init__(self, parent, name, fitpage_index, data_id): + super().__init__(parent, name) + self._fitpage_index = fitpage_index + self._data_id = data_id + super().setData(0, 1, self) + + @property + def fitpage_index(self): + return self._fitpage_index + + @property + def data_id(self): + return self._data_id + +class DataItem(PlotPageItem): + def __init__(self, parent, name, fitpage_index, data_id, type_num): + super().__init__(parent, name, fitpage_index, data_id) + # self.type_num saves if the item is a data item or a fit item in the tree widget + # identifier=1 is for data and identifier=2 is for fit identifier=3 is for residuals + self._type_num = type_num + + @property + def type_num(self): + return self._type_num + diff --git a/plotting_refactor/DataTreeWidget.py b/plotting_refactor/DataTreeWidget.py new file mode 100644 index 0000000000..8db6bf3a59 --- /dev/null +++ b/plotting_refactor/DataTreeWidget.py @@ -0,0 +1,35 @@ +from DataTreeItems import DataItem +from PySide6.QtCore import QByteArray, QMimeData, QRect, Qt +from PySide6.QtGui import QDrag +from PySide6.QtWidgets import QTreeWidget + + +class DataTreeWidget(QTreeWidget): + """ + Tree widget that is appearing in the DataViewer. It represents data stored in the DataCollector by objects from + DataTreeItems. Instantiating of these DataTreeItems happens in the DataViewer. + """ + def __init__(self, dataviewer, datacollector): + super().__init__(parent=dataviewer) + self.datacollector = datacollector + self.setGeometry(QRect(10, 10, 391, 312)) + self.setDragEnabled(True) + self.setColumnCount(1) + self.setHeaderLabels(["Data Name"]) + + def startDrag(self, supportedActions: Qt.DropAction): + """ + Overwriting the startDrag from the normal QTreeWidget. When dragging the QTreeWidgetItem to another plot, + mimetypes ID and Type are used to store the dataset.data_id and the type_num. Type_num represents if the + item is a data, fit or residuals item. + """ + item = self.currentItem() + if item: + if isinstance(item.data(0, 1), DataItem): + drag = QDrag(self) + mimeData = QMimeData() + mimeData.setData('ID', QByteArray(str(item.data(0, 1).data_id))) + mimeData.setData('Type', QByteArray(str(item.data(0, 1).type_num))) + + drag.setMimeData(mimeData) + drag.exec(supportedActions) diff --git a/plotting_refactor/DataViewer.py b/plotting_refactor/DataViewer.py new file mode 100644 index 0000000000..2f3c7ff590 --- /dev/null +++ b/plotting_refactor/DataViewer.py @@ -0,0 +1,226 @@ +from DataCollector import DataCollector +from DataTreeItems import DataItem, PlotPageItem +from DataTreeWidget import DataTreeWidget +from PlotModifiers import ModifierColormap, ModifierLinecolor, ModifierLinestyle +from PlotTreeItems import PlotItem, PlottableItem, SubTabItem, TabItem +from PlotTreeWidget import PlotTreeWidget +from PlotWidget import PlotWidget +from PySide6 import QtWidgets +from UI.DataViewerUI import Ui_DataViewer + + +class DataViewer(QtWidgets.QWidget, Ui_DataViewer): + """ + Class for interface between Plotwidget and Datacollector. Processing of signals for plotting, + redrawing of existing plots, adding new plot modifiers ends here. + """ + def __init__(self, main_window): + """ + Main Window is used as a parameter in the constructor to be able to hand it further to the Datacollector which + can then directly read values from checkboxes and spinboxes for new model calculations. + + self.dataTreeWidget and self.plotTreeWidget represent data that exists in the DataCollector or existing plots + in the plot widget, respectively. + """ + super(DataViewer, self).__init__() + self.setupUi(self) + + self.main_window = main_window + self.datacollector = DataCollector() + + self.dataTreeWidget = DataTreeWidget(self, self.datacollector) + self.plotTreeWidget = PlotTreeWidget(self) + + self.cmdClose.clicked.connect(self.onShowDataViewer) + self.cmdAddModifier.clicked.connect(self.onAddModifier) + self.plotTreeWidget.dropSignal.connect(self.redraw) + + self.setupMofifierCombobox() + self.plot_widget = PlotWidget(self, self.datacollector) + + def create_plot(self, fitpage_index): + self.update_plot_tree(fitpage_index) + self.plot_widget.show() + self.plot_widget.activateWindow() + + def update_datasets_from_collector(self, fitpage_index: int): + """ + Collects datasets from the datacollector and adds them to the dataTreeWidget. Is called upon a plot or + calculation request from the mainwindow. Only adds the dataset with the corresponding fitpage_index. + """ + datasets = self.datacollector.datasets + already_exists = False + for i in range(self.dataTreeWidget.topLevelItemCount()): + if fitpage_index == self.dataTreeWidget.topLevelItem(i).data(0, 1).fitpage_index: + already_exists = True + + if not already_exists: + for dataset in datasets: + if fitpage_index == dataset.fitpage_index: + name = "Data from Fitpage " + str(fitpage_index) + data_id = dataset.data_id + item = PlotPageItem(self.dataTreeWidget, [name], fitpage_index, data_id) + item.setData(0, 1, item) + subitem_data = DataItem(item, ["Data"], fitpage_index, data_id, 1) + subitem_data.setData(0, 1, subitem_data) + if dataset.has_y_fit(): + subitem_fit = DataItem(item, ["Fit"], fitpage_index, data_id, 2) + subitem_fit.setData(0, 1, subitem_fit) + + self.dataTreeWidget.expandAll() + + def onShowDataViewer(self): + """ + Function for handling showing and hiding of the data viewer and the button for that in the main window + """ + if self.isVisible(): + self.hide() + self.main_window.cmdShowDataViewer.setText("Show Data Viewer") + else: + self.show() + self.main_window.cmdShowDataViewer.setText("Hide Data Viewer") + + def update_dataset(self, fitpage_index, create_fit, checked_2d): + """ + Updates existing or non-existing datasets in the datacollector for a fitpage in the mainwindow + """ + self.datacollector.update_dataset(self.main_window, fitpage_index, create_fit, checked_2d) + self.update_datasets_from_collector(fitpage_index) + + def update_plot_tree(self, fitpage_index): + """ + Function to populate the plotTreeWidget for a certain fitpage. Checks if a plot for the given fitpage already + exists and recreates it if so. Therefore it collects all the data for the given fitpage from the datacollector + and creates the Tabs, Subtabs, Plots, Plottables for the plotTreeWidget. This mechanism also checks, if a + dataitem that comes from the datacollector is 2d. If it is 2d, the type_num for this PlottableItem will be + different (4 instead of 1) and the SubTabs.py can recognize, that only this 2d data can be plotted in one + actual plot. + """ + # check if an item for the fitpage index already exists + # if one is found - remove from tree + for i in range(self.plotTreeWidget.topLevelItemCount()): + if isinstance(self.plotTreeWidget.topLevelItem(i), TabItem): + if fitpage_index == self.plotTreeWidget.topLevelItem(i).data(0, 1).fitpage_index: + self.plotTreeWidget.takeTopLevelItem(i) + + # add tab + tab_name = "Plot for Fitpage " + str(fitpage_index) + tab_item = TabItem(self.plotTreeWidget, [tab_name], fitpage_index) + tab_item.setData(0, 1, tab_item) + + # add data child and corresponding plot children in every case + subtab_data = SubTabItem(tab_item, ["Data"], fitpage_index, 0) + subplot_data = PlotItem(subtab_data, ["Data Plot"], fitpage_index, 0, 0, + self.datacollector.get_data_by_fp(fitpage_index).is_data_2d) + fitpage_id = self.datacollector.get_data_by_fp(fitpage_index).data_id + + # create plottables in the plottreewidget with indicators (type_nums) to identify what kind of plot it is while + # plotting in subtabs.py: type_num = 1 : 1d data, type_num = 2 : 1d fit, type_num = 3 : 1d residuals + # type_num = 4 : 2d data, type_num = 5 : 2d fit, type_num = 6 : 2d residuals + # 2d plots cannot overlap each other as curves can do + # for every 2d data an additional plot is added and 1 plottable is inserted + if self.datacollector.get_data_by_fp(fitpage_index).is_data_2d: + plottable_data = PlottableItem(subplot_data, ["2d " + str(fitpage_id)], fitpage_id, 4) + else: + plottable_data = PlottableItem(subplot_data, [str(fitpage_id)], fitpage_id, 1) + + #add fit and residuals in case it was generated + if self.datacollector.get_data_by_fp(fitpage_index).has_y_fit(): + # on the fit tab: one central plot that shows the dataset and the according fit curve + # create tab for fit and residual plot + subtab_fit = SubTabItem(tab_item, ["Fit"], fitpage_index, 1) + subtab_residuals = SubTabItem(tab_item, ["Residuals"], fitpage_index, 2) + # if the data is 2d, then every plot contains only one plottable + if self.datacollector.get_data_by_fp(fitpage_index).is_data_2d: + subplot_data_subtab_fit = PlotItem(subtab_fit, ["Data"], fitpage_index, 1, 0, True) + plottable_subplot_data_subtab_fit = PlottableItem(subplot_data_subtab_fit, ["2d Plottable Fit Data"], fitpage_id, 4) + + subplot_fit_subtab_fit = PlotItem(subtab_fit, ["Fit"], fitpage_index, 1, 1, True) + plottable_subplot_fit_subtab_fit = PlottableItem(subplot_fit_subtab_fit, ["2d Plottable Fit Fit"], fitpage_id, 5) + + + subplot_data_subtab_residuals = PlotItem(subtab_residuals, ["Data"], fitpage_index, 2, 0, True) + plottable_subplot_data_subtab_residuals = PlottableItem(subplot_data_subtab_residuals, ["2d Plottable Residuals Data"], fitpage_id, 4) + + subplot_fit_subtab_residuals = PlotItem(subtab_residuals, ["Fit"], fitpage_index, 2, 1, True) + plottable_subplot_fit_subtab_residuals = PlottableItem(subplot_fit_subtab_residuals, ["2d Plottable Residuals Fit"], fitpage_id, 5) + + subplot_residuals_subtab_residuals = PlotItem(subtab_residuals, ["Residuals"], fitpage_index, 2, 2, True) + plottable_subplot_residuals_subtab_residuals = PlottableItem(subplot_residuals_subtab_residuals, ["2d Plottable Residuals Residuals"], fitpage_id, 6) + + else: # if the data is 1d, multiple plottables can be plotted in one plot + subplot_fit = PlotItem(subtab_fit, ["Fit Plot"], fitpage_index, 1, 0, False) + plottable_fit_data = PlottableItem(subplot_fit, ["Plottable Fit Data"], fitpage_id, 1) + plottable_fit_fit = PlottableItem(subplot_fit, ["Plottable Fit Fit"], fitpage_id, 2) + + # on the residuals subtab: create 2 plots with 3 datasets: on the top plot is the data and the fit, + # on the bottom plot is the residuals displayed with the same x-axis for comparison + subplot_residuals_fit = PlotItem(subtab_residuals, ["Fit Plot"], fitpage_index, 2, 0, False) + plottable_res_data = PlottableItem(subplot_residuals_fit, ["Plottable Res Data"], fitpage_id, 1) + plottable_res_fit = PlottableItem(subplot_residuals_fit, ["Plottable Res Fit"], fitpage_id, 2) + + subplot_res = PlotItem(subtab_residuals, ["Residuals Plot"], fitpage_index, 2, 1, False) + plottable_res = PlottableItem(subplot_res, ["Plottable Residuals"], fitpage_id, 3) + + self.plotTreeWidget.expandAll() + self.redraw(fitpage_index, 0) + + def redraw(self, redraw_fitpage_index, redraw_subtab_index): + """ + Redraws all tabs in the plotTreeWidget. parameters redraw_fitpage_index and redraw_subtab_index are used to show + the subtab for which the redrawAll was invoked, because a modifier was dragged onto a child plot or plottable + item in the plotTreeWidget. + If redrawing is invoked from the update_plot_tree method, only the fitpage_index will be used but 0 for + the subplot. + """ + if self.plotTreeWidget.topLevelItemCount() != 0: + for i in range(self.plotTreeWidget.topLevelItemCount()): + if isinstance(self.plotTreeWidget.topLevelItem(i).data(0, 1), TabItem): + self.plot_widget.redrawTab(self.plotTreeWidget.topLevelItem(i)) + + plotpage_index = self.datacollector.get_plotpage_index(redraw_fitpage_index) + self.plot_widget.setCurrentIndex(plotpage_index) + self.plot_widget.widget(plotpage_index).setCurrentIndex(redraw_subtab_index) + + def remove_plottree_item(self, index: int): + """ + Remove toplevelitem from plottreeitem upon closing a tab in the plottreewidget. + """ + # search for the existing dataset with the right plotpage index + datasets = self.datacollector.datasets + for dataset in datasets: + if dataset.plotpage_index == index: + fitpage_index_tab = dataset.fitpage_index + + # look through the toplevel items for the item with the right fitpage_index, that needs to be deleted. + for i in range(self.plotTreeWidget.topLevelItemCount()): + if self.plotTreeWidget.topLevelItem(i).data(0, 1).fitpage_index == fitpage_index_tab: + self.plotTreeWidget.takeTopLevelItem(i) + + + def onAddModifier(self): + """ + Add modifiers via button press to the plotTreeWidget. These can then be dragged around on PlotItems and + PlottableItems. Logic for dragging is in the PlotTreeWidget.py. + """ + currentmodifier = self.comboBoxModifier.currentText() + if 'color' in currentmodifier: + mod = ModifierLinecolor(self.plotTreeWidget, [currentmodifier]) + if 'linestyle' in currentmodifier: + mod = ModifierLinestyle(self.plotTreeWidget, [currentmodifier]) + if 'scheme' in currentmodifier: + mod = ModifierColormap(self.plotTreeWidget, [currentmodifier]) + def setupMofifierCombobox(self): + """ + Gives all the different available modifiers to the combobox so that they can be created by user selection. + """ + self.comboBoxModifier.addItem("color=r") + self.comboBoxModifier.addItem("color=g") + self.comboBoxModifier.addItem("color=b") + self.comboBoxModifier.addItem("linestyle=solid") + self.comboBoxModifier.addItem("linestyle=dashed") + self.comboBoxModifier.addItem("linestyle=dotted") + self.comboBoxModifier.addItem("scheme=jet") + self.comboBoxModifier.addItem("scheme=spring") + self.comboBoxModifier.addItem("scheme=gray") + diff --git a/plotting_refactor/Dataset.py b/plotting_refactor/Dataset.py new file mode 100644 index 0000000000..ea70ae20f0 --- /dev/null +++ b/plotting_refactor/Dataset.py @@ -0,0 +1,81 @@ +import time + + +class Dataset: + """ + Generic dataset class to hold all of the generated data for one fitpage with its generated id in one place. + The generated id is a timestamp with the fitpage number that the dataset belongs to as a prefix. + """ + def __init__(self, fitpage_index: int, x_data: list, y_data: list, y_fit: list, is_data_2d: list[list], + plotpage_index: int = 0): + self._fitpage_index = fitpage_index + self._x_data = x_data + self._y_data = y_data + self._y_fit = y_fit + self._plotpage_index = plotpage_index + self._is_data_2d = is_data_2d + self._data_id = self.__generate_id(self._fitpage_index) + + def __generate_id(self, fitpage_index: int): + a = str(int(time.time())) + b = len(a) + new_id = int(str(fitpage_index) + a[5:b]) + return new_id + + @property + def data_id(self) -> int: + return self._data_id + + @property + def fitpage_index(self) -> int: + return self._fitpage_index + + @property + def x_data(self) -> list: + return self._x_data + + @property + def y_data(self) -> list: + return self._y_data + + @property + def y_fit(self) -> list: + return self._y_fit + + def has_y_fit(self) -> bool: + if self._y_fit.size == 0: + return False + else: + return True + + @property + def plotpage_index(self) -> int: + return self._plotpage_index + + @plotpage_index.setter + def plotpage_index(self, plotpage_index: int): + if isinstance(plotpage_index, int): + self._plotpage_index = plotpage_index + else: + print("no integer") + + @x_data.setter + def x_data(self, x_data: list): + self._x_data = x_data + + @y_data.setter + def y_data(self, y_data: list): + self._y_data = y_data + + @y_fit.setter + def y_fit(self, y_fit: list): + self._y_fit = y_fit + + @property + def is_data_2d(self) -> bool: + return self._is_data_2d + + @is_data_2d.setter + def is_data_2d(self, is_data_2d: bool): + self._is_data_2d = is_data_2d + diff --git a/plotting_refactor/FitPage.py b/plotting_refactor/FitPage.py new file mode 100644 index 0000000000..c875a8a59f --- /dev/null +++ b/plotting_refactor/FitPage.py @@ -0,0 +1,38 @@ +from PySide6 import QtWidgets +from UI.FitPageUI import Ui_fitPageWidget + + +class FitPage(QtWidgets.QWidget, Ui_fitPageWidget): + """ + Widget that is shown in the tabs from the Mainwindow. Is a subclass of a widget to directly store fitpage indexes + in it. + """ + def __init__(self, identifier: int): + super(FitPage, self).__init__() + self.setupUi(self) + + #identifier keeps track of which number this fitpage is identifier by (it is incremental) + self._identifier = identifier + + self.comboBoxFormFactor.addItems(["Sphere", "Cylinder"]) + self.doubleSpinBox_height.setDisabled(True) + self.comboBoxFormFactor.currentIndexChanged.connect(self.index_changed) + + @property + def identifier(self) -> int: + return self._identifier + + def get_combobox_index(self) -> int: + return self.comboBoxFormFactor.currentIndex() + + def get_checkbox_fit(self) -> bool: + return self.checkBoxCreateFit.isChecked() + + def get_checkbox_2d(self) -> bool: + return self.checkBox2dData.isChecked() + + def index_changed(self, selected_item: int): + if selected_item == 0: + self.doubleSpinBox_height.setDisabled(True) + elif selected_item == 1: + self.doubleSpinBox_height.setDisabled(False) diff --git a/plotting_refactor/MainWindow.py b/plotting_refactor/MainWindow.py new file mode 100644 index 0000000000..95520a411b --- /dev/null +++ b/plotting_refactor/MainWindow.py @@ -0,0 +1,83 @@ +import sys +import traceback + +from DataViewer import DataViewer +from FitPage import FitPage +from PySide6 import QtWidgets +from UI.MainWindowUI import Ui_MainWindow + + +class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): + """ + MainWindow for the application, uses self.fittingTabs to create a tab selection of FitPages in which + comboboxes and spinboxes are placed for data creation. Also has calculation and plot buttons to invoke methods + for these logics. + Owner of the DataViewer, that centralizes the logic between plotting and data handling (with the DataCollector) + """ + def __init__(self): + super(MainWindow, self).__init__() + self.setupUi(self) + + self.setWindowTitle("Tabbed Plot Demo") + self.setFixedSize(700, 560) + + self.fitPageCounter = 1 + self.fittingTabs.addTab(FitPage(self.fitPageCounter), "Fit Page "+str(self.fitPageCounter)) + + self.dataviewer = DataViewer(self) + + self.cmdShowDataViewer.clicked.connect(self.dataviewer.onShowDataViewer) + self.cmdPlot.clicked.connect(self.onPlot) + self.cmdCalculate.clicked.connect(self.onCalculate) + self.actionNewFitPage.triggered.connect(self.onActionNewFitPage) + + def onPlot(self): + """ + Invoked when pressing plot button, collects the fitpage_index for the currently selected fitpage and gives it + to other parts of the program where this is used as a unique identifier for datasets that are saved in the + DataCollector. + Invokes plot creation after data creation. + """ + fitpage_index = self.fittingTabs.currentWidget().identifier + self.onCalculate() + self.dataviewer.create_plot(fitpage_index) + + def onCalculate(self): + """ + Calculates data for the currently selected fitpage. This data is then shown in the DataViewer dataTreeWidget. + """ + fitpage_index = self.fittingTabs.currentWidget().identifier + create_fit = self.fittingTabs.currentWidget().get_checkbox_fit() + checked_2d = self.fittingTabs.currentWidget().get_checkbox_2d() + self.dataviewer.update_dataset(fitpage_index, create_fit, checked_2d) + + def onActionNewFitPage(self): + """ + Creates a new fitpage by the button in the menubar of the mainwindow on top. + """ + self.fitPageCounter += 1 + self.fittingTabs.addTab(FitPage(self.fitPageCounter), "Fit Page " + str(self.fitPageCounter)) + self.fittingTabs.setCurrentIndex(self.fitPageCounter-1) + + def closeEvent(self, event): + QtWidgets.QApplication.closeAllWindows() + sys.exit() + +def excepthook(exc_type, exc_value, exc_tb): + tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) + print("error caught!:") + print("error message:\n", tb) + QtWidgets.QApplication.quit() + + +def main(): + sys.excepthook = excepthook + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + window.show() + + ret = app.exec() + sys.exit(ret) + +if __name__ == '__main__': + main() diff --git a/plotting_refactor/PlotModifiers.py b/plotting_refactor/PlotModifiers.py new file mode 100644 index 0000000000..a1583cddd7 --- /dev/null +++ b/plotting_refactor/PlotModifiers.py @@ -0,0 +1,32 @@ +from PySide6.QtWidgets import QTreeWidgetItem + + +class PlotModifier(QTreeWidgetItem): + def __init__(self, parent, name): + super().__init__(parent, name) + self.setData(0, 1, self) + +class ModifierLinestyle(PlotModifier): + def __init__(self, parent, name): + super().__init__(parent, name) + + def clone(self): + copy = super().clone() + return ModifierLinestyle(copy.parent(), [copy.text(0)]) + +class ModifierLinecolor(PlotModifier): + def __init__(self, parent, name): + super().__init__(parent, name) + + def clone(self): + copy = super().clone() + return ModifierLinecolor(copy.parent(), [copy.text(0)]) + + +class ModifierColormap(PlotModifier): + def __init__(self, parent, name): + super().__init__(parent, name) + + def clone(self): + copy = super().clone() + return ModifierColormap(copy.parent(), [copy.text(0)]) diff --git a/plotting_refactor/PlotTreeItems.py b/plotting_refactor/PlotTreeItems.py new file mode 100644 index 0000000000..329b75c878 --- /dev/null +++ b/plotting_refactor/PlotTreeItems.py @@ -0,0 +1,77 @@ +from PySide6.QtWidgets import QTreeWidgetItem + + +class TabItem(QTreeWidgetItem): + """ + Class for representation in the PlotTreeWidget. Saves the fitpage index to know, which data needs to be plotted + in the redrawing process of this tab. + """ + def __init__(self, parent, name, fitpage_index): + super().__init__(parent, name) + self._fitpage_index = fitpage_index + super().setData(0, 1, self) + + @property + def fitpage_index(self): + return self._fitpage_index + +class SubTabItem(TabItem): + """ + Class for representation in the PlotTreeWidget. Has both fitpage index (from the parent TabItem) and subtab_index + for plotting purposes in the redrawing process. + """ + def __init__(self, parent, name, fitpage_index, subtab_index): + super().__init__(parent, name, fitpage_index) + self._subtab_index = subtab_index + + @property + def subtab_index(self): + return self._subtab_index + +class PlotItem(SubTabItem): + """ + Class for representation in the PlotTreeWidget. Has fitpage_index and subtab_index from the parent items. _ax_index + and _is_plot_2d class attributes are used when the PlotTreeWidget item is drawn for redrawing. + """ + def __init__(self, parent, name, fitpage_index, subtab_index, ax_index, is_plot_2d): + super().__init__(parent, name, fitpage_index, subtab_index) + self._ax_index = ax_index + self._is_plot_2d = is_plot_2d + + @property + def ax_index(self): + return self._ax_index + + @property + def is_plot_2d(self): + return self._is_plot_2d + +class PlottableItem(QTreeWidgetItem): + """ + Class for representation in the PlotTreeWidget. Has _data_id and _type_num for replotting purposes in the redrawing + process. + """ + def __init__(self, parent, name, data_id, type_num): + super().__init__(parent, name) + self._data_id = data_id + # type serves the same purpose as in DataTreeItems - knowing if the item is a data item or a fit item, so that + # for example the axes can be scaled accordingly + self._type_num = type_num + super().setData(0, 1, self) + + @property + def data_id(self): + return self._data_id + + @property + def type_num(self): + # type_num = 1: 1d data, + # type_num = 2: 1d fit, + # type_num = 3: 1d residuals, + # type_num = 4 : 2d data, + # type_num = 5 : 2d fit, + # type_num = 6 : 2d residuals + return self._type_num + + + diff --git a/plotting_refactor/PlotTreeWidget.py b/plotting_refactor/PlotTreeWidget.py new file mode 100644 index 0000000000..69f1fd9044 --- /dev/null +++ b/plotting_refactor/PlotTreeWidget.py @@ -0,0 +1,129 @@ +import ctypes + +from PlotModifiers import PlotModifier +from PlotTreeItems import PlotItem, PlottableItem +from PySide6.QtCore import QByteArray, QMimeData, QRect, Signal +from PySide6.QtGui import QDrag +from PySide6.QtWidgets import QTreeWidget + + +class PlotTreeWidget(QTreeWidget): + dropSignal = Signal(int, int) + def __init__(self, DataViewer): + super().__init__(parent=DataViewer) + self.setGeometry(QRect(10, 332, 391, 312)) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + self.setColumnCount(1) + self.setHeaderLabels(["Plot Names"]) + + def startDrag(self, supportedActions): + """ + Function for starting the drag of modifiers across the PlotTreeWidget. + """ + item = self.currentItem() + + if item: + if isinstance(item, PlotModifier): + drag = QDrag(self) + mimeData = QMimeData() + mimeData.setData('Modifier', QByteArray(str(id(item)))) + + drag.setMimeData(mimeData) + drag.exec(supportedActions) + + def dragEnterEvent(self, event): + event.acceptProposedAction() + + + def dragMoveEvent(self, event): + """ + Function for checking if a drop should be accepted or not. DataItems from the DataTreeWidget are allowed and + the drop is accepted, if they are dropped onto a plot item. + Drops of modifiers are allowed if the modifier is dragged onto a PlotItem or a PlottableItem + TODO: This needs some change, since 1d modifiers should only be droppable onto Plottables and 2d only on Plots. + """ + targetItem = self.itemAt(event.position().toPoint()) + if targetItem is not None: + if event.mimeData().hasFormat('ID'): + if isinstance(targetItem, PlotItem): + event.acceptProposedAction() + else: + event.ignore() + elif event.mimeData().hasFormat('Modifier'): + if isinstance(targetItem, PlotItem): + event.acceptProposedAction() + elif isinstance(targetItem, PlottableItem): + event.acceptProposedAction() + else: + event.ignore() + else: + event.ignore() + else: + event.ignore() + + def dropEvent(self, event): + """ + Processing of the drop of either a modifier from this widget or a data item from the DataTreeWidget. + """ + + # Check, if the drag item contains "ID", then we can be sure that this comes from the DataTreeWidget + # (this might not be best practice, but with the serialization of the pointer as it is now, is seemed to me + # like it was the easiest method to achieve this) + if event.mimeData().data('ID'): + + # Re-collect the data from these streams in the drop + data_id = event.mimeData().data('ID').data() + data_type = event.mimeData().data('Type').data() + + # get the targetItem from the drop position + targetItem = self.itemAt(event.position().toPoint()) + + # if the target is a PlotItem, we deserialize the address of the pointer and create a new item from the + # object that is behind the pointer. + if isinstance(targetItem.data(0, 1), PlotItem): + new_plottable = PlottableItem(targetItem, [str(data_id.decode('utf-8'))], + int(data_id), int(data_type)) + + # get the fitpage index and the subtab index of the targetItem, so that they can be activated upon redrawing + redraw_fitpage_index = targetItem.data(0, 1).fitpage_index + redraw_subtab_index = targetItem.data(0, 1).subtab_index + + elif isinstance(targetItem.data(0, 1), PlottableItem): + # as soon as plottable slots are there, they can be filled in here + pass + + # the drop signal can be emitted now so that the tab where something was dragged onto can be redrawn. + self.dropSignal.emit(redraw_fitpage_index, redraw_subtab_index) + event.acceptProposedAction() + + # Here, the serialization also plays a role, because the modifier is cloned in the process and used to create + # a new child that the modifier will be for the target item. + elif event.mimeData().data('Modifier'): + data_address = int(event.mimeData().data('Modifier').data()) + data = ctypes.cast(data_address, ctypes.py_object).value + clone = data.clone() + targetItem = self.itemAt(event.position().toPoint()) + + if isinstance(targetItem.data(0, 1), PlottableItem): + targetItem.addChild(clone) + redraw_fitpage_index = targetItem.parent().data(0, 1).fitpage_index + redraw_subtab_index = targetItem.parent().data(0, 1).subtab_index + + elif isinstance(targetItem.data(0, 1), PlotItem): + targetItem.addChild(clone) + redraw_fitpage_index = targetItem.data(0, 1).fitpage_index + redraw_subtab_index = targetItem.data(0, 1).subtab_index + + self.dropSignal.emit(redraw_fitpage_index, redraw_subtab_index) + event.acceptProposedAction() + + else: + event.ignore() + + + + + + diff --git a/plotting_refactor/PlotWidget.py b/plotting_refactor/PlotWidget.py new file mode 100644 index 0000000000..3c3029fe34 --- /dev/null +++ b/plotting_refactor/PlotWidget.py @@ -0,0 +1,56 @@ +from PySide6.QtWidgets import QTabWidget +from SubTabs import SubTabs + + +class PlotWidget(QTabWidget): + def __init__(self, dataviewer, datacollector): + super().__init__() + self.setWindowTitle("Plot Widget") + self.setMinimumSize(600, 600) + + self.setTabsClosable(True) + + self.dataviewer = dataviewer + self.datacollector = datacollector + + self.tabCloseRequested.connect(self.closeTab) + + def closeTab(self, index: int): + """ + Action that is executed, when a tab is closed in the plotwidget. + """ + self.removeTab(index) + + self.dataviewer.remove_plottree_item(index) + + def widget(self, index) -> SubTabs: + return super().widget(index) + + def redrawTab(self, tabitem): + # check if the tab is already existing. + # if it is not existing: create it. otherwise: recalculate the tab + fitpage_index = tabitem.fitpage_index + plot_index = self.datacollector.get_data_by_fp(fitpage_index).plotpage_index + if plot_index == -1: + self.datacollector.set_plot_index(fitpage_index, self.count()) + self.addTab(SubTabs(self.datacollector, tabitem), + "Plot for FitPage " + str(fitpage_index)) + else: + self.removeTab(plot_index) + self.insertTab(plot_index, SubTabs(self.datacollector, tabitem), + "Plot for FitPage " + str(fitpage_index)) + + def get_subtabs(self, fitpage_index): + for i in range(self.count()): + if fitpage_index == self.widget(i).fitpage_index: + return self.widget(i) + + def get_figures(self, fitpage_index): + for i in range(self.count()): + if fitpage_index == self.widget(i).fitpage_index: + return self.widget(i).figures + + + + + diff --git a/plotting_refactor/RandomDatasetCreator.py b/plotting_refactor/RandomDatasetCreator.py new file mode 100644 index 0000000000..91ec0230ba --- /dev/null +++ b/plotting_refactor/RandomDatasetCreator.py @@ -0,0 +1,67 @@ +import numpy as np +from scipy import special +from scipy.optimize import curve_fit + + +class DatasetCreator: + """ + Class to generate data that can be used by the demo plots. + self.combobox_index remains a class variable, because then the fitting algorithm can use the same method in + differrent cases. + """ + def __init__(self): + self.combobox_index = -1 + + def func_2d(self, q, scale, radius, height): + """ + add 2d function that represents 2d "simulated" data in the 2d plots + """ + x = self.func(q[0], scale, radius, height) + y = self.func(q[1], scale, radius, height) + z = np.matmul(x, y) + return z + + def func(self, q, scale, radius, height): + """ + function for generating data with either spherical functions (case self.combobox_index== 0) or bessel functions + (case self.combobox_index == 1) + """ + if self.combobox_index == 0: + volume = 4 / 3 * np.pi * radius**3 + return scale / volume * (3*volume*(np.sin(q*radius)-q*radius*np.cos(q*radius)) / (q*radius)**3)**2 + elif self.combobox_index == 1: + volume = height * np.pi * radius**2 + return 4 * scale * volume * (special.jv(1, q*radius))**2 / (q*radius)**2 + + def createRandomDataset(self, scale, radius, height, combobox_index, fit=False, second_dimension=False): + """ + Creates a dataset with x, y and y_fit values, that use a 1d fitting algorithm. errors are applied to the data. + """ + self.combobox_index = combobox_index + size = 100 + intensity_fit = np.array([]) + if second_dimension: + q = np.linspace(start=1, stop=10, num=size).reshape(size, 1) + q_vec = (q, q.T) + intensity_no_err = self.func_2d(q_vec, scale, radius, height) + err = intensity_no_err * (np.random.random() - 0.5) * 2 + intensity = intensity_no_err + err + if fit: + intensity_1d = intensity.reshape(size*size) + q_1d = np.tile(q, size) + q_stack = np.vstack((q_1d, q_1d)) + p_opt, p_cov = curve_fit(f=self.func_2d, xdata=q_stack, ydata=intensity_1d, p0=(1.5, 1.5, 1.5)) + intensity_fit = self.func_2d(q_vec, *p_opt) + q = np.meshgrid(q, q) + + else: + q = np.linspace(start=1, stop=10, num=size) + intensity = np.empty(shape=size) + intensity_no_err = self.func(q, scale, radius, height) + err = (np.random.random(size)-0.5) * intensity_no_err + + intensity = intensity_no_err + err + if fit: + p_opt, p_cov = curve_fit(f=self.func, xdata=q, ydata=intensity, p0=(1.5, 1.5, 1.5)) + intensity_fit = self.func(q, *p_opt) + return q, intensity, intensity_fit diff --git a/plotting_refactor/SubTabs.py b/plotting_refactor/SubTabs.py new file mode 100644 index 0000000000..0a91759a0e --- /dev/null +++ b/plotting_refactor/SubTabs.py @@ -0,0 +1,191 @@ + +import matplotlib.figure +import numpy as np +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg, NavigationToolbar2QT +from PlotModifiers import ModifierColormap, ModifierLinecolor, ModifierLinestyle, PlotModifier +from PlotTreeItems import PlottableItem +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QDockWidget, QMainWindow, QTabWidget, QVBoxLayout, QWidget + + +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() + +class SubTabs(QTabWidget): + """ + Class for keeping subtabs and adding figures with subplots to them. It takes a tabitem to process and iterates + over all the existing children of the given tabitem to plot their contents in the respective order. For example + for every child item of the TabItem, one subtab will be created and for every child item of the subtab, one plot + will be generated. + The application of modifiers onto plots is also managed in this class constructor. + """ + def grayOutOnDock(self, dock_container: QMainWindow, dock_widget: QDockWidget): + """ + Function that is connected to the topLevelChanged slot of the dock widget for the plot widget. 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(): + print("gray") + dock_container.setStyleSheet("QMainWindow#" + name + " { background-color: gray }") + else: + print("white") + dock_container.setStyleSheet("QMainWindow#" + name + " { background-color: white }") + + def __init__(self, datacollector, tabitem): + super().__init__() + + self.datacollector = datacollector + self.figures: list[matplotlib.figure] = [] + # iterate through subtabs + for i in range(tabitem.childCount()): + # add subplots + layout = QVBoxLayout() + figure = matplotlib.figure.Figure(figsize=(5, 5)) + canvas = ClickableCanvas(figure) + layout.addWidget(canvas) + layout.addWidget(NavigationToolbar2QT(canvas)) + + # decide whether there is only one plot needs to be plotted. then, only one central plot is needed + subplot_count = tabitem.child(i).childCount() + if subplot_count == 1: + ax = 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 + ax = [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 = 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 = [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(figure.add_subplot(sub_gridspec[idx])) + + # after the subplots are created, the axes objects need to be filled with actual lines/2d plots + # iterate through subplots + for j in range(tabitem.child(i).childCount()): + # set the title of the plot with the subplot name of the PlotTreeWidget item + ax[j].set_title(str(tabitem.child(i).child(j).text(0))) + + # iterate through plottables and plot modifiers (PlotTreeWidget items) + for k in range(tabitem.child(i).child(j).childCount()): + + plottable_or_modifier_item = tabitem.child(i).child(j).child(k).data(0, 1) + # check if the plottable or modifier item is a PlottableItem (actual data to be displayed) + if isinstance(plottable_or_modifier_item, PlottableItem): + plottable = plottable_or_modifier_item + dataset = self.datacollector.get_data_by_id(plottable.data_id) + + # if the dataset is 2d, plotting will be done with a heatmap plot + if dataset.is_data_2d: + + # collect a possible existing colormap plot modifier (child item) + # and save it, so that it can be used during plot creation + colormap_modifier = "" + for ii in range(plottable.childCount()): + if isinstance(plottable.child(ii), ModifierColormap): + colormap_modifier = plottable.child(ii).text(0).split('=')[1] + if colormap_modifier == "": + colormap_modifier = "jet" + + # get the data from the dataset for the plot + x = dataset.x_data + y = dataset.y_data + y_fit = dataset.y_fit + + # check if the plot is a data plot (4), fit plot (5) or residual plot (6) + if plottable.type_num == 4: + ax[j].pcolor(x[0], x[1], y, + norm=matplotlib.colors.LogNorm(vmin=np.min(y), + vmax=np.max(y)), + cmap=colormap_modifier) + elif plottable.type_num == 5: + ax[j].pcolor(x[0], x[1], y_fit, + norm=matplotlib.colors.LogNorm(vmin=np.min(y_fit), + vmax=np.max(y_fit)), + cmap=colormap_modifier) + elif plottable.type_num == 6: + y_res = np.absolute(np.subtract(y_fit, y)) + ax[j].pcolor(x[0], x[1], y_res, + norm=matplotlib.colors.LogNorm(vmin=np.min(y_res), + vmax=np.max(y_res)), + cmap=colormap_modifier) + + # if it is not a 2d plot, it must be a 1d plot (line plot) + else: + # select again for data plot (1), fit plot (2) and residual plot (3) + if plottable.type_num == 1: # data plot: log-log plot, show only data + ax[j].plot(dataset.x_data, dataset.y_data) + ax[j].set_yscale('log') + elif plottable.type_num == 2: # fit plot: log-log plot, show fit and data curve + ax[j].plot(dataset.x_data, dataset.y_fit) + ax[j].set_yscale('log') + elif plottable.type_num == 3: # residual plot lin-log plot, show calc and show res only + ax[j].plot(dataset.x_data, np.subtract(dataset.y_fit, dataset.y_data)) + + # iterate through plottable modifier, e.g. linecolor, linestyle + for l in range(plottable.childCount()): + plottable_modifier = plottable.child(l) + if isinstance(plottable_modifier.data(0, 1), ModifierLinecolor): + ax[j].get_lines()[-1].set_color(plottable_modifier.text(0).split('=')[1]) + elif isinstance(plottable_modifier.data(0, 1), ModifierLinestyle): + ax[j].get_lines()[-1].set_linestyle(plottable_modifier.text(0).split('=')[1]) + + # applying a colormap to a set of lines and setting the respective color to lines that are + # returned by the axes object + elif isinstance(plottable_or_modifier_item, PlotModifier): + plot_modifier = plottable_or_modifier_item + if isinstance(plot_modifier, ModifierColormap): + cmap = matplotlib.colormaps[plot_modifier.text(0).split('=')[1]] + n = len(ax[j].get_lines()) + for m in range(n): + ax[j].get_lines()[m].set_color(cmap(m/(n-1))) + + # create the widget that will be inside the dock widget + figure.tight_layout() + canvas_widget = QWidget() + canvas_widget.setLayout(layout) + + # create the main window, which is the container for the dock widget, so that it can be dragged out and + # put in again + dock_container = QMainWindow() + # set the object name for later, so that the style sheet changes for graying out only affects the dock + # container itself and not the child widgets of the dock container. fitpage_index is used as an identifier + # here + dock_container.setObjectName("DockContainer" + str(tabitem.data(0, 1).fitpage_index)) + dock_widget = QDockWidget() + + dock_widget.topLevelChanged.connect(lambda x: self.grayOutOnDock(dock_container, dock_widget)) + + dock_widget.setWidget(canvas_widget) + dock_container.addDockWidget(Qt.DockWidgetArea.TopDockWidgetArea, dock_widget) + + self.addTab(dock_container, tabitem.child(i).text(0)) + self.figures.append(figure) + + diff --git a/plotting_refactor/UI/DataViewerUI.ui b/plotting_refactor/UI/DataViewerUI.ui new file mode 100644 index 0000000000..aee94e7002 --- /dev/null +++ b/plotting_refactor/UI/DataViewerUI.ui @@ -0,0 +1,67 @@ + + + DataViewer + + + + 0 + 0 + 700 + 680 + + + + + 700 + 680 + + + + + 700 + 680 + + + + Data Viewer + + + + + 620 + 650 + 75 + 24 + + + + Close + + + + + + 10 + 650 + 241 + 22 + + + + + + + 260 + 650 + 91 + 24 + + + + Add Modifier + + + + + + diff --git a/plotting_refactor/UI/FitPageUI.ui b/plotting_refactor/UI/FitPageUI.ui new file mode 100644 index 0000000000..413f347d8a --- /dev/null +++ b/plotting_refactor/UI/FitPageUI.ui @@ -0,0 +1,164 @@ + + + fitPageWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + 80 + 100 + 62 + 22 + + + + 0.000000000000000 + + + 1000.000000000000000 + + + 1.000000000000000 + + + + + + 80 + 130 + 62 + 22 + + + + 0.000000000000000 + + + 1000.000000000000000 + + + 1.000000000000000 + + + + + + 20 + 100 + 51 + 16 + + + + Scale + + + + + + 20 + 130 + 51 + 16 + + + + Radius + + + + + + 20 + 70 + 211 + 16 + + + + Parameters for scattering function P(q) + + + + + + 20 + 20 + 131 + 22 + + + + + + + 80 + 160 + 62 + 22 + + + + 0.000000000000000 + + + 1000.000000000000000 + + + 1.000000000000000 + + + + + + 20 + 160 + 51 + 16 + + + + Height + + + + + + 210 + 20 + 181 + 20 + + + + Create Fit for generated Data + + + + + + 210 + 40 + 171 + 20 + + + + Generate 2D data + + + + + + diff --git a/plotting_refactor/UI/MainWindowUI.ui b/plotting_refactor/UI/MainWindowUI.ui new file mode 100644 index 0000000000..8ea013bd5b --- /dev/null +++ b/plotting_refactor/UI/MainWindowUI.ui @@ -0,0 +1,96 @@ + + + MainWindow + + + + 0 + 0 + 700 + 560 + + + + MainWindow + + + + + + 500 + 490 + 91 + 24 + + + + Calculate + + + + + + 10 + 10 + 681 + 471 + + + + -1 + + + + + + 10 + 490 + 111 + 24 + + + + Show Data Viewer + + + + + + 600 + 490 + 91 + 24 + + + + Plot + + + + + + + 0 + 0 + 700 + 22 + + + + + Fit Pages + + + + + + + + + New Fit Page + + + + + + diff --git a/plotting_refactor/__init__.py b/plotting_refactor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plotting_refactor/notes_and_sketches/Agenda.txt b/plotting_refactor/notes_and_sketches/Agenda.txt new file mode 100644 index 0000000000..662616c309 --- /dev/null +++ b/plotting_refactor/notes_and_sketches/Agenda.txt @@ -0,0 +1,122 @@ +Possible Todos for the demo: (I will not put effort into realizing these, just ideas and corrections) +1 Residuals for a fit need to be plotted beneath the Fit and therefore the grid structure of the plotting needs change +2 Real data linking could be something to look into (right now, there is only the identifier) +3 Dragging modifiers across the PlotTreeWidget causes trouble, because it does not autoscroll for whatever reason + This could maybe be fixed with using a debugger + It is possible to start the application normally and connect a debugger to the running instance afterwards +4 Delete button for modifiers + +Current working branch for the integration of the demo into SasView is: plotting_refactor_integration +This branch (plotting_refactor_tabs) is with a standalone example in the plotting_refactor folder + To start the demo, plotting_refactor/MainWindow.py needs to be executed + + +Other todos that were mentioned and could need integration in SasView in general: +The button for killing a Fitpage is way too small when executing SasView in a remote windows/linux instance + Is there the possibility to have a prompt if the current Fitpage should really be closed? + The showing of this prompt could be enabled/disabled in the settings + +Lionel had a request with integration of a functionality similar to this: +https://github.com/criosx/molgroups/tree/main + + +Information regarding SasView that helps to understand the plotting process a little better: +PlotterWidget extends PlotterBase (PlotterWidget is a class in the file Plotter.py) +PlotterBase extends QWidget (uses for example QWidget.show or QWidget.showNormal) +PlotterBase gets its manager from PlotterWidget +PlotterWidget is instantiated in DataExplorerWindow method plotData (ll.1205) +DataExplorerWindow is given as the parent and the manager to the PlotterWidget +DataExplorerWindow has the MainWindow as its parent, the GuiManager as its guimanager and the GuiManager._data_manager as its manager + +GuiManager.communicate is an instance of GuiUtils.Communicate() +GuiUtils.Communicate is a utility class for tracking the Qt Signals and connecting all of them to the right end +GuiUtils.Communicate extends QtCore.QObject. This class defines all the signals that are needed and processed (ll.83f.) +Signals are connected to everything in GuiManager.addCallbacks which executed when instantiating the GuiManager + in the start of the SasView MainWindow (MainWindow.py) + + +Short overview on the plotting process: +What happens when clicking the "Plot" button in the FittingWidget in Sasview? +(1) FittingWidget.py -> onPlot -> showPlot/showTheoryPlot -> _requestPlots -> goes to tabbedPlotWidget among other things + -> throws Signal to GuiManager +(2) Signal for Guimanager.showPlotFromName or Guimanager.showPlot redirects to + to DataExplorerWidget.displayData or DataExplorerWidget.displayDataByName +(3) displayData ends in either active_plots.showNormal(4.1) or plotData(4.2) or updatePlot(4.3) or appendOrUpdatePlot(4.4) +(4.1) active_plots.showNormal is just a QWidget method that shows the Widget in its restored original size +(4.2) plotData creates a PlotterWidget instance and sets the item of it to the given item of the method (DataExplorer ll.1205) +(4.3) updatePlot goes to PlotterWidget.replacePlot: + This updates the bookkeeping dictionary self.active_plots, where all the PlotterWidget items that are currently + needed are saved +(4.4) appendOrUpdatePlot checks if the given plot already exists in the plot_dict of the PlotterWidget and depending on + that, it either uses PlotterWidget.plot or PlotterWidget.replacePlot + +Strategy for integration of the demo (at first) + +Using methods that are already existing in SasView to feed all of the existing infrastructure + into the new TabbedPlotWidget +Therefore there have to be places in the code where existing classes like for example DataExplorer, Plotter, + PlotterBase, GuiManager, FittingWidget are communicating with the new TabbedPlotWidget and tell them about the + requested plots and all the other information that is provided when fitting/simulating something. + These places in the code will feed the TabbedPlotWidget. + +Example: displayData is used as a method not to plot something directly but to manage what should be plotted and +then give all that is needed to another method that plots. This can be used to not only plot in the original infra- +structure, but also plot the same in the TabbedPlotWidget + +Therefore, some of the classes can interact with tabbedPlotWidget (which is a variable/object of GuiManager right now) +and tell it to do something. The classes therefore interact via its parent with self.parent.tabbedPlotWidget or similar + +In order to use the existing infrastructure, it is important to understand the reason behind all the methods that are +included into the plotting process and then understand how this can be used to feed all the needed information for +a similar plotting behaviour in the TabbedPlotWidget. + +Overview: How to get to the TabbedFittingWidget from some classes that might need access: + from Dataexplorer: self.parent.tabbedPlotWidget + from Plotter: self.parent.parent.tabbedPlotWidget + from PlotterBase: self.parent.parent.tabbedPlotWidget + from GuiManager: self.tabbedPlotWidget + + +Dictionaries for plot management: +DataExplorerWindow.active_plots: all of the PlotterWidget instances are added in here to keep track of the windows? +PlotterWidget.plot_dict: keep track of all the datasets that have been printed in this particular window, so that no + other window with the same dataset is displayed? (this is just a conjecture at the moment) + +Structure of the Subtabs(which extends QTabWidget): +-> Tabs are DockContainers(QMainWindows) +-> setting the widget of the DockContainers with the docking zone to CanvasWidgets(QWidgets) +-> CanvasWidgets are filled with layouts that have only one widget at the moment: a ClickableCanvas(FigureCanvasQtAgg) +-> ClickableCanvas has the figure and extends a FigureCanvasQtAgg that can act upon events on the canvas like clicking + +Problem/Challenge: The figure for the canvas and the layout for the CanvasWidget(which can include a matplotlib + navigation toolbar if this is needed) have to be there before creating all the objects. Then one can give the layout + and the figure into the creation and set them this way + +Problem/Challenge: Existing Axes objects from Matplotlib that come with all the other handy stuff (title, labels, ...) +cannot be transferred to other figures very easily. Therefore, the created Axes from the Plotter cannot just be +given back up the stream and then be integrated into the TabbedPlotWidget + Possible solution: When plotting something in the TabbedPlotWidget, just create an axes and feed it into the + plotting functionality of the Plotter afterwards. Then, all the steps are made on the right axes (again) +Does this solution need to know, how many plots there are beforehand? + + +Problems with the current solution of with plotting on both axes at the same time: +1 In one of the Axes, Data and Fit are plotted twice +2 Legends are breaking stuff with subplots because of the width of them +3 The first plot seems to be empty always + -> This has maybe something to do with showing Fit+Data on another axes and thus this gets ignored? + -> For example when fitting a Data1d: for the merged fit and dataset plot, the for loop in plot dats is executed + twice instead of only once, because there is only one axes for it in there + this is also the case for the normal axes that exists in the qwidget, but it leads to too many axes being produced + by using len(plots) itself as an indicator for how many axes need to be generated in SubTabs + + Solution could be: first iterating over the len(plots and check in there, how many plots are really needed +4 2d plots do not work at all + + +SubTabs need to be actually applied, right now, all plots are plotted in the first SubTab, but the SubTabs as a + QTabWidget are not actually used, everything is just stored in the first one. + This possibly also means, that parts of the code with the figure need to be refactored already, since + every Tab in the SubTab would need its own figure which could be complicated to be accessed through all the layers + from the QTabWidget SubTabs itself. +appendOrUpdatePlot in DataExplorer needs refactoring to work with the TabbedPlotWidget diff --git a/plotting_refactor/notes_and_sketches/Questions.txt b/plotting_refactor/notes_and_sketches/Questions.txt new file mode 100644 index 0000000000..c6be66bf8b --- /dev/null +++ b/plotting_refactor/notes_and_sketches/Questions.txt @@ -0,0 +1,29 @@ +What will the Window with all the tabs for plots be in terms of implementation? + Is the window permanent? Like a perspective for instance? + Is it belonging to the DataExplorerWindow? + +Same lifetime as SasView +Showable and Hideable when opening other perspectives such as the invariant +All the data should pass through the dataexplorer, plotwidget is owned by dataexplorer +QStandardItemModels are used in the DataExplorer.py to keep the loaded data and the theory models +QStandardItemModels can store data so that i can be directly accessed through qt (like a tree structure) +Track the dictionary, as the dictionary changes, change the model as well + +Follow the model and the theory_model from DataExplorerWindow throughout +other parts of the code and keep track of what is happening to it, how it is accessed +and how it is edited + + + +How does the checking for an existing tab with locals() work? l.1206 sas.qtgui.MainWindow.DataExplorer +locals as keeping track of what has been declared in this method call + +Is the data manager really redundant and are all the functionalities moved to GuiManager? +Qt is able to stream stuff without carrying a pointer to the Datamanager everywhere it is needed + + +Does it work that create_gui_data creates a new Data1D or Data2D element and parses the data into these +items? then the new_plot items can be worked with and it can also have an id + +Communicate class in GuiUtils can collect signals from parts of the codes +where signals are not processed in the same class but throughout different classes \ No newline at end of file diff --git a/plotting_refactor/notes_and_sketches/Report and Technical Documentation.docx b/plotting_refactor/notes_and_sketches/Report and Technical Documentation.docx new file mode 100644 index 0000000000..ba43c3710c Binary files /dev/null and b/plotting_refactor/notes_and_sketches/Report and Technical Documentation.docx differ diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_05_17.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_05_17.txt new file mode 100644 index 0000000000..5e455c3215 --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_05_17.txt @@ -0,0 +1,25 @@ +How can trends be combined with the plotting functionality? +Residuals displayed in another subplot on the bottom. - done +unique ID by timestamp - done, implmented with current time and fitpageindex as a sum + + + +historical: +Plot refactoring was kicked off at code camp isis +concrete plans where there in jan 24 during the code camp + +what is planned in other branches at the time: +generic data formats +views in addition to data formats +- data object lives in a view +- in addition: measurement time or detector distance are added to the view as metadata that are there for every measurement of this set + +trends for packaging data: +- multiple datasets on the same instrument for different temeperatures or pressures for example + + +ideas for the demo: +tree like structure for data and plots (almost done) +Dataviewer owns plot widget for easier dataflow (done) +combobox instead of new window for sending to subplot (done) + diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_05_24.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_05_24.txt new file mode 100644 index 0000000000..fbe250c2aa --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_05_24.txt @@ -0,0 +1,34 @@ +plot refactoring meeting 24.05.2024 + +arbitrary number of plots in one subplot window? e.g. 2x2? + +polydispersity subplot for single parameters + +for a trend: show subplots all in one subplot? like in a grid? + +dataselector tree structure -move data around +sasview drag and drop mechanism for dataexplorer droppable data load widget +SAS qtgui mainwindow + + +mockup for subplots in one minor subplot window + +2dviewing for data and fit and residuals, 3 of them side by side? +datatab - only data very big +fittab - data and fit +residualstab - all three + +dockable possibility for tabs and subtabs + + + + +lin scale for the residuals + + + +2d plotting different 2d scaling for different detectors on the same instrument + + + + diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_05_31.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_05_31.txt new file mode 100644 index 0000000000..00b61d42ed --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_05_31.txt @@ -0,0 +1,21 @@ +2D mockup for next week + + +highlight a plot by clicking and then it is enlarged and when you click another one it goes back + how can the plots be recognized + +manipulations for one plot so that it is only in that particular plot + + +sasview beta is coming out soon + +inside the qtreewidget one can access the class of the original item that was added to the tree by + using data or something + +qstandarditems have children that keep the objects that are originally fed + + +shared intensity bar for all plots + + +push my project to git diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_06_04.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_06_04.txt new file mode 100644 index 0000000000..7d37843b3a --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_06_04.txt @@ -0,0 +1,3 @@ +lucas demo is very good with the modifiers + +batch slicing - showing a slicer right next to the 2d \ No newline at end of file diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_06_14.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_06_14.txt new file mode 100644 index 0000000000..b6e07a80b6 --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_06_14.txt @@ -0,0 +1,21 @@ +stream object without Qdatastream but directly using the object + +transfer towards pyside instead of pyqt6 + +2D plotting + Generate mock-ups (done 5/31) + Create functional systems based on mock ups + 3 plotting groups + Group 1 - main plot group - Initially show fit with residuals and data as subplots (all with the same color scale) + Clicking on one of the subplots swaps it with the primary plot + Group 2 - multiple main plots with the same color scale bar + Group 3 - multiple main plots with different scale bars + Each plot/subplot could be moved/added to any other group and scale bars would automatically update +Drag-drop plots + Drag any plot(s) into a plot subgroup + Visual feedback where the plot(s) would go (e.g. a template box would appear where the plot will go as the plot group is dragged) +Polydispertiy plots and where they should go +plan for next week: + - continue with d&d (with no datastream, if possible) + - start with 2D implementation based on the general mockup + - dockable tabs for fitting diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_06_21.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_06_21.txt new file mode 100644 index 0000000000..cba8cd649b --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_06_21.txt @@ -0,0 +1,16 @@ +lucas new student on july 22nd + +user feedback for dragging onto plot tree widget items (possible or not possible) + + +start 2d intensity implementing +for 2d clicking: +matplotlib can interact with the mouse clicks as well + + +make a draft pull request and open up a directory for my files + +add screenshots for the 2d plots + + +general plotting container where anything can be modified in any fashion? \ No newline at end of file diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_07_05.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_07_05.txt new file mode 100644 index 0000000000..fbc478207d --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_07_05.txt @@ -0,0 +1,13 @@ +layout has more than one item remaining and thats why i cannot shrink further + +talking about the trend objects + +Thread objects for creating the data (few layers of abstraction in between) +sasmodels kernel is a good starting point +what it uses and what it is used by + +combining both functionalities from tabbed click and other demo + +modifier problem + + diff --git a/plotting_refactor/notes_and_sketches/plot_refactoring_07_12.txt b/plotting_refactor/notes_and_sketches/plot_refactoring_07_12.txt new file mode 100644 index 0000000000..49cb285d45 --- /dev/null +++ b/plotting_refactor/notes_and_sketches/plot_refactoring_07_12.txt @@ -0,0 +1,16 @@ +lucas student starting soon + +data has convolution for resolutions and e.g. hankel transformation +builtin for models to go from the real model to the data + +comparing 2 models together? +bumps- parallelizing model calculation, jeff is going to raffle? code camp to work on this + +how is the data serialized? +does an earlier fit and data object live on, if the fitpage is recalculated? +how can changes to an object be kept in storage? stacks? + +json objects in the background? +tree or order in which the stuff happened? list of changes is temporary? + +where does the save and load live? - on main sasview \ No newline at end of file diff --git a/plotting_refactor/notes_and_sketches/sasview_plotting_sktech.png b/plotting_refactor/notes_and_sketches/sasview_plotting_sktech.png new file mode 100644 index 0000000000..215392bac7 Binary files /dev/null and b/plotting_refactor/notes_and_sketches/sasview_plotting_sktech.png differ diff --git a/plotting_refactor/notes_and_sketches/thoughts.txt b/plotting_refactor/notes_and_sketches/thoughts.txt new file mode 100644 index 0000000000..ed49ee3a9c --- /dev/null +++ b/plotting_refactor/notes_and_sketches/thoughts.txt @@ -0,0 +1,94 @@ +demo integration into sasview: + +what is needed? +DataIDs from DataViewer/DataCollector, where are these saved in SasView? +What does the current displaying system look like? + +How is a fit linked with the underlying data? +How are residuals linked? +How is polydispersity linked? +First layer of the model QStandardItemModel would be Data nodes + children of these nodes would be e.g. the polydispersity, fit, residuals, etc. + + +Relation datasets/fitpages <--> existing plots? +How is the current logic behind plotting something new and replotting an existing plot +Checking if certain id already exists in the model/the dictionary for plotted objects + +plotting tools for 1d and 2d exist and can keep functioning that way? +They just produce QWidgets that can be embedded inside the tabs afterwards +GuiManager-filesWidget.plotData creates an instance of PlotterWidget, which is a QWidget and + + +Where are the QWidgets from the Plotting generated? Where is explicitly show() stated? + +dataID generation is in sas.qtgui.MainWindow.DataManager on line 124 +The ID is a cocatenated string of the data object name +(another GUI-only element that will move to sasdata) and +1 plus the time stamp when SasView​ was loaded, not when the data file was loaded. +The time stamp will be relative to the data load time, +not the loader instance creation time when fully implemented into +the sasdata object. + + +What is a model? +How can the plot for a model with a certain name already exist in the GuiUtils, +if the plotRequested Signal has not been emitted in the first place? +Why is _requestPlots applying plot.plot_role != DataRole.ROLE_DATA and only giving signals where this is not true/true? +Where does self.communicate.plotRequestedSignal go? What does this signal invoke? + self.communicate is coming from the parent through initialize globals + in initializeGlobals, the self.communicate = self.parent.communicate + communicate comes from GuiUtils.Communicate()? + How is this initialized if i create a new fitpage by hitting new fitpage? + Where is the original GuiUtils coming from? -maybe from GuiManager? + + + + + + +Tree of what happens if python run.py is executed: +run.py + run.__main__ + sas.cli.main + sas.qtgui.MainWindow.MainWindow.run_sasview() + QApplication is setup + QtReactor is installed -> sasview can then listen to client/server calls + and process them. + this is handled in sas.qtgui.Utilities.ReactorCore.install(), + where install is only defined at the very bottom and is dependent + on the system the user is using to start sasview (win32 or else?) + mainwindow is initialized with mainwindow = MainSasViewWindow(): + self.guimanager = GuiManager(self) (it says main sassview window functionality itself) + ALOT of stuff is done and a lot of stuff is imported + self._datamanger = DataManager() + self.addWidgets() + What signal callbacks? l.106 + addCallbacks() l.670 + self.communicate = GuiUtils.Communicate() + a big bunch of signals that are connected with methods + e.g. self.communicate.plotRequestedSignal.connect(self.showPlot) + showPlot(self, plot, id) + self.filesWidget.displayData(plot, id) + What Categories? + All categories are listed in (user folder) + ~/sasview/categories.json and are built there + by the sas.qtgui.utilities.categoryinstaller.categoryinstaller + And all the existing models come from + sas.sascalc.fit.models.ModelManager.cat_model_list() + this gives back a listed variant of all models + that are existing in the standard library. but it only goes through + sasmodels.sasmodels.sasview_model.load_standard_models() (and before it goes through sasview.src.sas.sascalc.fit.models.ModelManagerBase and ModelManagers + what action triggers? l116 + + + +Where does the data come from? +DataManager manages all the data that is loaded from a certain path and the ones +from creation at loading time +DataManager.create_gui_data can receive data from loader and create a data to use for guiframe +This will depend on Data2D and Data1D objects from PlotterData + + + + \ No newline at end of file