From 638508ca80410622be1d87ab3bd9736c1c7c22ea Mon Sep 17 00:00:00 2001 From: Olivier Jossoud <olivier.jossoud@lsce.ipsl.fr> Date: Mon, 14 Oct 2019 16:14:15 +0200 Subject: [PATCH] Explo. Color, offset, mult_factor, timeshift and X/Y original OK. --- pyqt/mainwindow.ui | 20 ++- src/dataprovider/exploprovider.py | 6 +- src/gui/uimainwindow.py | 20 ++- src/uim/explouim.py | 258 ++++++++++++++++++++++++------ 4 files changed, 239 insertions(+), 65 deletions(-) diff --git a/pyqt/mainwindow.ui b/pyqt/mainwindow.ui index 780ec8b..4122394 100644 --- a/pyqt/mainwindow.ui +++ b/pyqt/mainwindow.ui @@ -61,17 +61,17 @@ </column> <column> <property name="text"> - <string>Vertical line</string> + <string>Step curve</string> </property> </column> <column> <property name="text"> - <string>Cursor</string> + <string>Vertical line</string> </property> </column> <column> <property name="text"> - <string>Curve</string> + <string>Cursor</string> </property> </column> <column> @@ -91,12 +91,22 @@ </column> <column> <property name="text"> - <string>Time shift</string> + <string>Time shift (s)</string> + </property> + </column> + <column> + <property name="text"> + <string>Visible</string> + </property> + </column> + <column> + <property name="text"> + <string>X (orig.)</string> </property> </column> <column> <property name="text"> - <string>Delete</string> + <string>Y (orig.)</string> </property> </column> </widget> diff --git a/src/dataprovider/exploprovider.py b/src/dataprovider/exploprovider.py index ea0dc05..0dec51d 100644 --- a/src/dataprovider/exploprovider.py +++ b/src/dataprovider/exploprovider.py @@ -61,7 +61,7 @@ class Dataset: # Get dataset name self.dataset_text = directory_name[-9:] - self.last_data_datetime = datetime.datetime(1970, 1, 1) + self.last_data_datetime = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) self.instlogs = {} @@ -118,6 +118,7 @@ class InstrumentInstantLog(InstrumentLog): def __get_df__(self) -> pd.DataFrame: df = pd.read_csv(self.full_file_name, sep=",", parse_dates=["datetime"]) + df["datetime"] = df["datetime"].dt.tz_localize('UTC') return df def get_variables(self): @@ -125,7 +126,6 @@ class InstrumentInstantLog(InstrumentLog): def get_timeseries(self, variable: str) -> pd.DataFrame: timeseries_df = self.df[self.df["name"] == variable] - timeseries_df.rename(columns={'value': variable}, inplace=True) timeseries_df = timeseries_df.drop(columns=['name']) return timeseries_df @@ -137,6 +137,7 @@ class InstrumentPeriodicLog(InstrumentLog): def __get_df__(self) -> pd.DataFrame: df = pd.read_csv(self.full_file_name, sep="\t", parse_dates=["datetime"]) + df["datetime"] = df["datetime"].dt.tz_localize('UTC') return df def get_variables(self): @@ -146,4 +147,5 @@ class InstrumentPeriodicLog(InstrumentLog): def get_timeseries(self, variable: str) -> pd.DataFrame: timeseries_df = self.df[["datetime", variable]] + timeseries_df.rename(columns={variable: 'value'}, inplace=True) return timeseries_df diff --git a/src/gui/uimainwindow.py b/src/gui/uimainwindow.py index 486d78a..7b0c14e 100644 --- a/src/gui/uimainwindow.py +++ b/src/gui/uimainwindow.py @@ -25,7 +25,7 @@ class Ui_MainWindow(object): self.explo_tablewidget_variables = QtWidgets.QTableWidget(self.tab_explo) self.explo_tablewidget_variables.setGeometry(QtCore.QRect(20, 50, 1021, 201)) self.explo_tablewidget_variables.setObjectName("explo_tablewidget_variables") - self.explo_tablewidget_variables.setColumnCount(10) + self.explo_tablewidget_variables.setColumnCount(12) self.explo_tablewidget_variables.setRowCount(0) item = QtWidgets.QTableWidgetItem() self.explo_tablewidget_variables.setHorizontalHeaderItem(0, item) @@ -47,6 +47,10 @@ class Ui_MainWindow(object): self.explo_tablewidget_variables.setHorizontalHeaderItem(8, item) item = QtWidgets.QTableWidgetItem() self.explo_tablewidget_variables.setHorizontalHeaderItem(9, item) + item = QtWidgets.QTableWidgetItem() + self.explo_tablewidget_variables.setHorizontalHeaderItem(10, item) + item = QtWidgets.QTableWidgetItem() + self.explo_tablewidget_variables.setHorizontalHeaderItem(11, item) self.horizontalLayoutWidget_2 = QtWidgets.QWidget(self.tab_explo) self.horizontalLayoutWidget_2.setGeometry(QtCore.QRect(20, 10, 221, 31)) self.horizontalLayoutWidget_2.setObjectName("horizontalLayoutWidget_2") @@ -183,11 +187,11 @@ class Ui_MainWindow(object): item = self.explo_tablewidget_variables.horizontalHeaderItem(1) item.setText(_translate("MainWindow", "Variable")) item = self.explo_tablewidget_variables.horizontalHeaderItem(2) - item.setText(_translate("MainWindow", "Vertical line")) + item.setText(_translate("MainWindow", "Step curve")) item = self.explo_tablewidget_variables.horizontalHeaderItem(3) - item.setText(_translate("MainWindow", "Cursor")) + item.setText(_translate("MainWindow", "Vertical line")) item = self.explo_tablewidget_variables.horizontalHeaderItem(4) - item.setText(_translate("MainWindow", "Curve")) + item.setText(_translate("MainWindow", "Cursor")) item = self.explo_tablewidget_variables.horizontalHeaderItem(5) item.setText(_translate("MainWindow", "Color")) item = self.explo_tablewidget_variables.horizontalHeaderItem(6) @@ -195,9 +199,13 @@ class Ui_MainWindow(object): item = self.explo_tablewidget_variables.horizontalHeaderItem(7) item.setText(_translate("MainWindow", "Mult. factor")) item = self.explo_tablewidget_variables.horizontalHeaderItem(8) - item.setText(_translate("MainWindow", "Time shift")) + item.setText(_translate("MainWindow", "Time shift (s)")) item = self.explo_tablewidget_variables.horizontalHeaderItem(9) - item.setText(_translate("MainWindow", "Delete")) + item.setText(_translate("MainWindow", "Visible")) + item = self.explo_tablewidget_variables.horizontalHeaderItem(10) + item.setText(_translate("MainWindow", "X (orig.)")) + item = self.explo_tablewidget_variables.horizontalHeaderItem(11) + item.setText(_translate("MainWindow", "Y (orig.)")) self.explo_label_dataset.setText(_translate("MainWindow", "Dataset:")) self.explo_pushbutton_add_row.setText(_translate("MainWindow", "+ Add Row")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_explo), _translate("MainWindow", "Data exploration")) diff --git a/src/uim/explouim.py b/src/uim/explouim.py index 53f838c..ad8b48a 100644 --- a/src/uim/explouim.py +++ b/src/uim/explouim.py @@ -1,9 +1,11 @@ import datetime import pyqtgraph as pg from PyQt5.QtWidgets import * +from PyQt5.QtGui import QColor from PyQt5.QtCore import * import pandas as pd import numpy as np +from pyqtgraph.GraphicsScene.mouseEvents import MouseClickEvent import utils from config import Config @@ -16,11 +18,26 @@ class ExploUim: # "Variables" tableWidget columns identifiers. INSTRUMENT_COL = 0 VARIABLE_COL = 1 - VLINE_COL = 2 - CURSOR_COL = 3 - CURVE_COL = 4 - COLOR_COL = 5 - DELETE_COL = 6 + CURVE_COL = 2 + VLINE_COL = 3 + CURSOR_COL = 4 + COLOR_COL = 5 + OFFSET_COL = 6 + MULT_COL = 7 + TIMESHIFT_COL = 8 + VISIBLE_COL = 9 + XORIG_COL = 10 + YORIG_COL = 11 + + DEFAULT_COLORS = [QColor(255, 255, 255), # White + QColor(228, 26, 28), # Red + QColor(55, 126, 184), # Blue + QColor(77, 175, 74), # Green + QColor(152, 78, 163), # Violet + QColor(255, 127, 0), # Orange + QColor(255, 255, 51), # Yellow + QColor(166, 86, 40), # Brown + QColor(247, 129, 191)] # Pink def __init__(self, explo_prvd: ExploProvider, main_ui: Ui_MainWindow, config: Config): self.main_ui = main_ui @@ -29,10 +46,17 @@ class ExploUim: self.current_dataset = None + # The "var_id" is used to identify and match table's lines and plot's items (curves, etc.). Table's row id can + # not be used because it changes when previous rows are deleted. + self.current_variable_id = 0 + self.__initialize_variable_table__() self.__initialize_plot__() self.__initialize_dataset_combobox__() + self.main_ui.explo_pushbutton_add_row.clicked.connect(self.__add_new_row_in_variable_table__) + self.main_ui.explo_tablewidget_variables.cellClicked.connect(self.__change_color__) + def __initialize_dataset_combobox__(self): """Populate the "datasets" combobox with the existing dataset directory names.""" data_root_dir = self.config.read("DATA_SOURCE", "absolute_root_dir") @@ -45,7 +69,8 @@ class ExploUim: def __update_current_dataset__(self, dataset_dir: str): self.current_dataset = self.explo_prvd.datasets[dataset_dir] - self.__update_instruments_comboboxes__() + for row_id in range(self.main_ui.explo_tablewidget_variables.rowCount()): + self.__update_instruments_combobox__(row_id) #################################################################################################################### # "Variables" table @@ -54,58 +79,100 @@ class ExploUim: """Initialize the table containing the to-be-displayed variables""" # Set column widths - self.main_ui.explo_tablewidget_variables.setColumnWidth(self.INSTRUMENT_COL, 90) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.INSTRUMENT_COL, 110) self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VARIABLE_COL, 90) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.CURVE_COL, 85) self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VLINE_COL, 95) - self.main_ui.explo_tablewidget_variables.setColumnWidth(self.CURSOR_COL, 80) - self.main_ui.explo_tablewidget_variables.setColumnWidth(self.CURVE_COL, 80) - self.main_ui.explo_tablewidget_variables.setColumnWidth(self.COLOR_COL, 80) - self.main_ui.explo_tablewidget_variables.setColumnWidth(self.DELETE_COL, 80) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.CURSOR_COL, 60) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.COLOR_COL, 55) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.OFFSET_COL, 90) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.MULT_COL, 90) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.TIMESHIFT_COL, 95) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VISIBLE_COL, 70) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.XORIG_COL, 80) + self.main_ui.explo_tablewidget_variables.setColumnWidth(self.YORIG_COL, 70) self.__add_new_row_in_variable_table__() def __add_new_row_in_variable_table__(self): """Add a new row in the calibration table. Initialize the cell's content and properties.""" - row_id = self.main_ui.explo_tablewidget_variables.rowCount() - self.main_ui.explo_tablewidget_variables.insertRow(row_id) + table = self.main_ui.explo_tablewidget_variables + row_id = table.rowCount() + table.insertRow(row_id) # Instruments instrument_item = QComboBox() - self.main_ui.explo_tablewidget_variables.setCellWidget(row_id, self.INSTRUMENT_COL, instrument_item) - self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.INSTRUMENT_COL).currentTextChanged.connect( + table.setCellWidget(row_id, self.INSTRUMENT_COL, instrument_item) + table.cellWidget(row_id, self.INSTRUMENT_COL).currentTextChanged.connect( lambda text, row_id=row_id: self.__update_variables_combobox__(combobox_text=text, row=row_id)) # Variables variable_item = QComboBox() - self.main_ui.explo_tablewidget_variables.setCellWidget(row_id, self.VARIABLE_COL, variable_item) - self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.VARIABLE_COL).currentTextChanged.connect( - lambda text, row_id=row_id: self.__apply_new_variable__(variable_name=text, row=row_id)) + table.setCellWidget(row_id, self.VARIABLE_COL, variable_item) + table.cellWidget(row_id, self.VARIABLE_COL).currentTextChanged.connect( + lambda text, row_id=row_id: self.__apply_variable_change__(row_id=row_id, variable_name=text)) + + # Step Curve + curve_item = QCheckBox() + table.setCellWidget(row_id, self.CURVE_COL, curve_item) # Vertical line vline_item = QCheckBox() - self.main_ui.explo_tablewidget_variables.setCellWidget(row_id, self.VLINE_COL, vline_item) + table.setCellWidget(row_id, self.VLINE_COL, vline_item) # Cursor cursor_item = QCheckBox() - self.main_ui.explo_tablewidget_variables.setCellWidget(row_id, self.CURSOR_COL, cursor_item) - - # Curve - curve_item = QCheckBox() - self.main_ui.explo_tablewidget_variables.setCellWidget(row_id, self.CURVE_COL, curve_item) + table.setCellWidget(row_id, self.CURSOR_COL, cursor_item) # Color color_item = QTableWidgetItem() - self.main_ui.explo_tablewidget_variables.setItem(row_id, self.COLOR_COL, color_item) - - # Delete - delete_item = QPushButton("Delete") - self.main_ui.explo_tablewidget_variables.setCellWidget(row_id, self.DELETE_COL, delete_item) - - def __update_instruments_comboboxes__(self): - instrument_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(0, self.INSTRUMENT_COL) # TODO: ALL rows!! + color_item.setBackground(self.DEFAULT_COLORS[row_id % len(self.DEFAULT_COLORS)]) + table.setItem(row_id, self.COLOR_COL, color_item) + + # Offset + offset_item = QDoubleSpinBox() + offset_item.setMinimum(-1000.0) + table.setCellWidget(row_id, self.OFFSET_COL, offset_item) + table.cellWidget(row_id, self.OFFSET_COL).valueChanged.connect( + lambda value, row_id=row_id: self.__apply_variable_change__(row_id=row_id)) + + # Multiplicative factor + mult_item = QDoubleSpinBox() + mult_item.setValue(1.0) + mult_item.setMinimum(-1000.0) + table.setCellWidget(row_id, self.MULT_COL, mult_item) + table.cellWidget(row_id, self.MULT_COL).valueChanged.connect( + lambda value, row_id=row_id: self.__apply_variable_change__(row_id=row_id)) + + # Time shift + timeshift_item = QDoubleSpinBox() + timeshift_item.setMinimum(-1000.0) + table.setCellWidget(row_id, self.TIMESHIFT_COL, timeshift_item) + table.cellWidget(row_id, self.TIMESHIFT_COL).valueChanged.connect( + lambda value, row_id=row_id: self.__apply_variable_change__(row_id=row_id)) + + # Visible + visible_item = QCheckBox() + visible_item.setChecked(True) + table.setCellWidget(row_id, self.VISIBLE_COL, visible_item) + + # X original + xorig_item = QTableWidgetItem() + table.setItem(row_id, self.XORIG_COL, xorig_item) + + # Y original + yorig_item = QTableWidgetItem() + table.setItem(row_id, self.YORIG_COL, yorig_item) + + self.__update_instruments_combobox__(row_id) + + def __update_instruments_combobox__(self, row_id: int): + instrument_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.INSTRUMENT_COL) instrument_combobox.clear() dataset_dir = self.main_ui.explo_combobox_dataset.currentText() + if dataset_dir == "": + return dataset = self.explo_prvd.datasets[dataset_dir] instrument_logs = dataset.instlogs @@ -123,23 +190,74 @@ class ExploUim: for variable_name in variable_names: variables_combobox.addItem(variable_name) - def __apply_new_variable__(self, variable_name: str, row: int): - # The variable combobox is cleard before variables of the newly-selected instrument are written. - # This function is called on variable-combobox edition, thus it is also called on combobox clear. - # So in this case, do nothing, wait for the call related to the filling of the combobox - if variable_name == "": + def __change_color__(self, row: int, column: int): + if column != self.COLOR_COL: return + color = QColorDialog.getColor() + self.main_ui.explo_tablewidget_variables.item(row, self.COLOR_COL).setBackground(color) + + def __get_row_dataframe__(self, row_id: int) -> pd.DataFrame: + table = self.main_ui.explo_tablewidget_variables + # Get instrument log - instrument_log_name = self.main_ui.explo_tablewidget_variables.cellWidget(row, self.INSTRUMENT_COL).currentText() + instrument_log_name = table.cellWidget(row_id, self.INSTRUMENT_COL).currentText() instrument_log = self.current_dataset.instlogs[instrument_log_name] + # Get variable name + variable_name = table.cellWidget(row_id, self.VARIABLE_COL).currentText() + timeseries = instrument_log.get_timeseries(variable_name) + + return timeseries + + def __apply_variable_change__(self, row_id: int, variable_name: str = None) -> None: + """Read the information related to the row_id and call plot function + + Parameters + ---------- + row_id + variable_name + """ + # The variable combobox is cleared before variables of the newly-selected instrument are written. + # This function is called on variable-combobox edition, thus it is also called on combobox clear. + # So in this case, do nothing, wait for the call related to the filling of the combobox + if variable_name == "": + return + + table = self.main_ui.explo_tablewidget_variables + + timeseries = self.__get_row_dataframe__(row_id) + + # Get color + color = table.item(row_id, self.COLOR_COL).background().color() + + # Get position adjustment variables (offset, multiplicative factor and time shift) + offset = table.cellWidget(row_id, self.OFFSET_COL).value() + mult = table.cellWidget(row_id, self.MULT_COL).value() + timeshift = table.cellWidget(row_id, self.TIMESHIFT_COL).value() + + try: - self.__update_plot__(timeseries, variable_name) + self.__update_plot__(timeseries, row_id, color, offset, mult, timeshift) except TypeError: print("Cannot plot that.") + def __update_xy_original__(self, instant: datetime.datetime): + table = self.main_ui.explo_tablewidget_variables + for row_id in range(table.rowCount()): + # Get X orig (original non-shifted datetime value) + timeshift_sec = table.cellWidget(row_id, self.TIMESHIFT_COL).value() + x_orig = instant - datetime.timedelta(seconds=timeshift_sec) + table.item(row_id, self.XORIG_COL).setText(x_orig.strftime("%H:%M:%S.%f")[:-5]) + + # Get Y orig (original non-shifted variable value) + df = self.__get_row_dataframe__(row_id).copy() + y_orig = df[df["datetime"] <= instant].iloc[-1]["value"] + if isinstance(y_orig, float): + y_orig = "{:.4f}".format(y_orig) + table.item(row_id, self.YORIG_COL).setText(y_orig) + #################################################################### # Plot @@ -147,25 +265,61 @@ class ExploUim: self.plot_item = pg.PlotItem(axisItems={'bottom': utils.TimeAxisItem(orientation='bottom')}) - self.curve = pg.PlotCurveItem() - self.plot_item.addItem(self.curve) + self.step_curves = dict() self.main_ui.explo_graphicsview_top.setCentralItem(self.plot_item) self.lines_items = [] - def __update_plot__(self, timeseries: pd.DataFrame, variable_name: str) -> None: + # Vertical line following the cursor + self.cursor_vline = pg.InfiniteLine(angle=90, movable=False) + self.plot_item.addItem(self.cursor_vline, ignoreBounds=True) + + def __update_plot__(self, timeseries: pd.DataFrame, row_id: int, color: QColor, + offset: float, mult: float, timeshift_sec: float) -> None: try: - timeseries[variable_name] = timeseries[variable_name].astype(float) + timeseries["value"] = timeseries["value"].astype(float) except ValueError: print("Value is not convertible to float") + return + + # Get the to-be-modified curve + if row_id in self.step_curves: + step_curve = self.step_curves[row_id] else: - last_datetime = self.current_dataset.last_data_datetime + datetime.timedelta(seconds=1) - timeseries['datetime'].at[max(timeseries.index)+1] = last_datetime - self.curve.setData(x=utils.pd_time_to_epoch_ms(timeseries['datetime']), - y=list(timeseries[variable_name]), - pen=(200, 200, 200), - stepMode=True, - symbol="o", - symbolBrush=(255, 0, 0), - symbolPen='w') + step_curve = pg.PlotCurveItem() + self.plot_item.addItem(step_curve) + step_curve.scene().sigMouseClicked.connect(lambda event: self.__mouse_clicked__(event)) + self.step_curves[row_id] = step_curve + + # As it is a _step_ curve, add a last datetime point to determine the end of the last step. This is the datatime + # of the last available data of the dataset, plus one second. + last_datetime = self.current_dataset.last_data_datetime + datetime.timedelta(seconds=1) + x_values = timeseries['datetime'].copy() + x_values = x_values.append(pd.Series([last_datetime])) + + # Apply time shift + x_values = x_values + datetime.timedelta(seconds=timeshift_sec) + + y_values = list(timeseries["value"]) + + # Apply multiplicative factor + y_values = [y * mult for y in y_values] + + # Apply Y-axis offset + y_values = [y + offset for y in y_values] + + # Set data to the plot + step_curve.setData(x=utils.pd_time_to_epoch_ms(x_values), + y=y_values, + pen=color, + stepMode=True) + + def __mouse_clicked__(self, event: MouseClickEvent): + pos = event.scenePos() + mousePoint = self.step_curves[0].getViewBox().mapSceneToView(pos) + instant = datetime.datetime.fromtimestamp(mousePoint.x(), tz=datetime.timezone.utc) + # instant = datetime.datetime.fromtimestamp(mousePoint.x()) + self.__update_xy_original__(instant) + self.cursor_vline.setPos(mousePoint.x()) + # print("x="+instant.strftime("%H:%M:%S")) -- GitLab