-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Plotting refactor #2940
base: main
Are you sure you want to change the base?
Plotting refactor #2940
Changes from 21 commits
d944176
4d07568
596bdec
9126553
2c87007
f6204f6
c816b34
7307146
4afeb29
5967b55
a57b4c2
b1d2be5
7871fd9
feda56a
788d85b
9484ec4
9f53141
62cde3a
d7b450d
6c6a320
85c1438
c86cab4
35a3531
d4316bb
1cdf37d
da3f350
0d0efa5
087ab1b
08dacdb
f7d9303
05ba173
ad0daf0
ee03473
f5eafef
5abe666
56154bc
082d5f0
921e9a1
99f205d
061a17f
aff5ca2
ba66be0
b6a7389
95d7722
f0b4411
0deb9bf
38bf454
2c10a86
f9360a0
a5f20f0
231e92c
c985f66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
from PySide6 import QtWidgets | ||
import RandomDatasetCreator | ||
from Dataset import Dataset | ||
|
||
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.get_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].set_x_data(x_data) | ||
self._datasets[existing_dataset_index].set_y_data(y_data) | ||
self._datasets[existing_dataset_index].set_y_fit(y_fit) | ||
self._datasets[existing_dataset_index].set_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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A better name would just be |
||
""" | ||
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.get_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.get_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.get_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.get_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.set_plotpage_index(plot_index) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from PySide6.QtWidgets import QTreeWidgetItem | ||
|
||
class PlotPageItem(QTreeWidgetItem): | ||
def __init__(self, parent, name, fitpage_index, data_id): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. types There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docstring |
||
super().__init__(parent, name) | ||
self.fitpage_index = fitpage_index | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These look like private variables, consider doing |
||
self.data_id = data_id | ||
super().setData(0, 1, self) | ||
|
||
def get_fitpage_index(self): | ||
return self.fitpage_index | ||
|
||
def get_data_id(self): | ||
return self.data_id | ||
|
||
class DataItem(PlotPageItem): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These do not need to inherit from one another. The |
||
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 | ||
|
||
def get_type_num(self): | ||
return self.type_num | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView | ||
from PySide6.QtCore import QMimeData, QRect, QByteArray, QDataStream, QIODevice | ||
from PySide6.QtGui import QDrag | ||
from DataTreeItems import DataItem | ||
|
||
class DataTreeWidget(QTreeWidget): | ||
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): | ||
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).get_data_id()))) | ||
mimeData.setData('Type', QByteArray(str(item.data(0, 1).get_type_num()))) | ||
|
||
drag.setMimeData(mimeData) | ||
drag.exec(supportedActions) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you drag these objects to places outside the window, what happens then? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
from PySide6 import QtWidgets | ||
|
||
from DataViewerUI import Ui_DataViewer | ||
from PlotWidget import PlotWidget | ||
from DataTreeWidget import DataTreeWidget | ||
from PlotTreeWidget import PlotTreeWidget | ||
from DataCollector import DataCollector | ||
from DataTreeItems import PlotPageItem, DataItem | ||
from PlotTreeItems import TabItem, SubTabItem, PlotItem, PlottableItem | ||
from PlotModifiers import ModifierLinecolor, ModifierLinestyle, ModifierColormap, PlotModifier | ||
|
||
|
||
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.redrawAll) | ||
|
||
self.setupMofifierCombobox() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
self.plot_widget = PlotWidget(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): | ||
""" | ||
Collects datasets from the datacollector and adds them to the dataTreeWidget. Is called upon a plot or | ||
calculation request from the mainwindow | ||
""" | ||
datasets = self.datacollector.datasets | ||
for i in range(len(datasets)): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
fitpage_index = datasets[i].get_fitpage_index() | ||
name = "Data from Fitpage " + str(fitpage_index) | ||
data_id = datasets[i].get_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 datasets[i].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.update_datasets_from_collector() | ||
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) | ||
|
||
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).get_fitpage_index(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be all on one line. Python evaluates each part of an if statement individually and from left to right, so there will be no cost or increase in errors by combining these.
|
||
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_2d()) | ||
fitpage_id = self.datacollector.get_data_by_fp(fitpage_index).get_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_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_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.redrawAll(fitpage_index, 0) | ||
|
||
def redrawAll(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 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") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add new line between blocks, if you like