-
JOSSOUD Olivier authoredJOSSOUD Olivier authored
explouim.py 30.27 KiB
import datetime
import re
import math
import pyqtgraph as pg
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QColor
from PyQt5.QtCore import *
import pandas as pd
from pandas.api.types import is_numeric_dtype
import numpy as np
from pyqtgraph.GraphicsScene.mouseEvents import MouseClickEvent
import utils
from config import Config
from dataprovider.exploprovider import ExploProvider
from gui.uimainwindow import Ui_MainWindow
from gui.stabwindow import StabWindow
class ExploUim:
# "Variables" tableWidget columns identifiers.
INSTRUMENT_COL = 0
VARIABLE_COL = 1
COLOR_COL = 2
OFFSET_COL = 3
MULT_COL = 4
TIMESHIFT_COL = 5
VISIBLE_COL = 6
XORIG_COL = 7
YORIG_COL = 8
DEFAULT_COLORS = [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, stab_window: StabWindow):
self.main_ui = main_ui
self.explo_prvd = explo_prvd
self.config = config
self.stab_window = stab_window
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_combobox_dataset.currentTextChanged.connect(self.__update_current_dataset__)
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__)
self.main_ui.explo_checkbox_manualevent.stateChanged.connect(self.__update_manual_event__)
# Save/load current setup
self.main_ui.explo_lineedit_setup_name.textChanged.connect(self.__check_setup_save_name__)
self.main_ui.explo_pushbutton_setup_save.clicked.connect(self.__save_setup__)
self.__refresh_existing_setups__()
self.main_ui.explo_listwidget_setup_list.itemClicked.connect(
lambda: self.main_ui.explo_pushbutton_setup_load.setEnabled(True))
self.main_ui.explo_pushbutton_setup_load.clicked.connect(self.__load_setup__)
# Stabilization time analysis
self.__init_stab__()
self.main_ui.explo_pushbutton_stab.clicked.connect(self.__show_stab_window__)
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")
dataset_dirs = self.explo_prvd.explore_root_directory(data_root_dir)
dataset_dirs.sort(reverse=True)
for directory in dataset_dirs:
self.main_ui.explo_combobox_dataset.addItem(directory)
self.__update_current_dataset__(dataset_dirs[0])
def __update_current_dataset__(self, dataset_dir: str, ask_confirmation: bool = True):
# Clear table and plot
self.main_ui.explo_tablewidget_variables.setRowCount(0)
self.__initialize_plot__()
# Change current dataset and add a new row
self.current_dataset = self.explo_prvd.datasets[dataset_dir]
self.__add_new_row_in_variable_table__()
# Enable/disable the possibility to display manual events, depending on the events log file availability.
self.main_ui.explo_checkbox_manualevent.setEnabled(self.current_dataset.manual_event_log is not None)
self.main_ui.explo_checkbox_manualevent.setChecked(False)
# Update 'existing setups'
self.__refresh_existing_setups__()
self.main_ui.explo_pushbutton_setup_load.setEnabled(False)
####################################################################################################################
# "Variables" table
def __initialize_variable_table__(self):
"""Initialize the table containing the to-be-displayed variables"""
# Set column widths
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.INSTRUMENT_COL, 130)
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.VARIABLE_COL, 110)
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, 90)
self.main_ui.explo_tablewidget_variables.setColumnWidth(self.YORIG_COL, 175)
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."""
table = self.main_ui.explo_tablewidget_variables
row_id = table.rowCount()
table.insertRow(row_id)
# Instruments
instrument_item = QComboBox()
table.setCellWidget(row_id, self.INSTRUMENT_COL, instrument_item)
# Variables
variable_item = QComboBox()
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))
# Connect Instrument change to variables display
table.cellWidget(row_id, self.INSTRUMENT_COL).currentTextChanged.connect(
lambda text, row_id=row_id: self.__update_variables_combobox__(combobox_text=text, combobox=variable_item))
# Color
color_item = QTableWidgetItem()
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)
table.cellWidget(row_id, self.VISIBLE_COL).stateChanged.connect(
lambda state, row_id=row_id: self.__apply_variable_change__(row_id=row_id))
# X original
xorig_item = QTableWidgetItem()
xorig_item.setFlags(Qt.ItemIsEnabled) # Read only
table.setItem(row_id, self.XORIG_COL, xorig_item)
# Y original
yorig_item = QTableWidgetItem()
yorig_item.setFlags(Qt.ItemIsEnabled) # Read only
table.setItem(row_id, self.YORIG_COL, yorig_item)
self.__update_instruments_combobox__(self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.INSTRUMENT_COL))
if hasattr(self, "plot_item"):
self.plot_item.getViewBox().enableAutoRange(enable=True)
def __update_instruments_combobox__(self, combobox: QComboBox):
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
instrument_logs_names = [log_name for log_name in instrument_logs]
instrument_logs_names.sort()
for instrument_name in instrument_logs_names:
combobox.addItem(instrument_name)
def __update_variables_combobox__(self, combobox_text: str, combobox: QComboBox):
if combobox_text == "":
return
# variables_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(row, self.VARIABLE_COL)
variables_combobox = combobox
variables_combobox.clear()
instrument_log_name = combobox_text
instrument_log = self.current_dataset.instlogs[instrument_log_name]
variable_names = instrument_log.get_variables()
for variable_name in variable_names:
variables_combobox.addItem(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)
self.__apply_variable_change__(row)
def __get_row_dataframe__(self, row_id: int) -> pd.DataFrame:
table = self.main_ui.explo_tablewidget_variables
# Get instrument log
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 variable visibility
visible = table.cellWidget(row_id, self.VISIBLE_COL).isChecked()
# 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, row_id, color, offset, mult, timeshift, visible)
except TypeError:
self.main_ui.statusbar.showMessage("Failed to plot [" + variable_name + "]", msecs=3000)
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()
df = df[df["datetime"] <= instant]
if len(df.index) == 0:
y_orig = "Out of range"
else:
y_orig = df.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
def __initialize_plot__(self) -> None:
self.plot_item = pg.PlotItem(axisItems={'bottom': utils.TimeAxisItem(orientation='bottom')})
self.step_curves = dict()
self.main_ui.explo_graphicsview_top.setCentralItem(self.plot_item)
self.lines_items = []
# Vertical lines for the manual events
self.event_vlines = []
self.event_texts = []
# Vertical line following the cursor
self.cursor_vline = pg.InfiniteLine(angle=90, movable=False, pen=pg.mkPen(style=Qt.DotLine))
self.plot_item.addItem(self.cursor_vline, ignoreBounds=True)
# Click-fixed vertical line to get measure
self.measure_vline = pg.InfiniteLine(angle=90, movable=False)
self.plot_item.addItem(self.measure_vline, ignoreBounds=True)
def __update_plot__(self, timeseries: pd.DataFrame, row_id: int, color: QColor,
offset: float, mult: float, timeshift_sec: float,
visible: bool) -> None:
# Get the to-be-modified curve
if row_id in self.step_curves:
step_curve = self.step_curves[row_id]
else:
step_curve = pg.PlotCurveItem()
self.step_curves[row_id] = step_curve
self.plot_item.addItem(step_curve)
step_curve.scene().sigMouseClicked.connect(lambda event: self.__mouse_clicked__(event))
step_curve.scene().sigMouseMoved.connect(lambda event: self.__mouse_moved__(event))
# Set curve visibility
if not visible:
step_curve.hide()
else:
step_curve.show()
# Set data to the plot
step_curve.setData(x=self.__get_timeseries_x_values__(timeseries, timeshift_sec),
y=self.__get_timeseries_y_values__(timeseries, offset, mult),
pen=color,
stepMode=True)
def __update_manual_event__(self, checked_state: int):
if checked_state == 2:
events_df = self.current_dataset.manual_event_log.get_timeseries("event")
for event in events_df.iterrows():
x_pos = utils.pd_time_to_epoch_ms([event[1]["datetime"]])[0]
# Vertical line
event_vline = pg.InfiniteLine(pos=x_pos,
angle=90,
movable=False,
pen=pg.mkPen(color=QColor(255, 255, 255)))
self.event_vlines.append(event_vline)
self.plot_item.addItem(event_vline, ignoreBounds=True)
# Text
event_str = event[1]["value"]
event_str = event_str.replace("\n", "<br>")
text_item = pg.TextItem(
html='<div style="text-align: center"><span style="color: #FFF;">' + event_str + '</div>',
# anchor=(-0.3, 0.5),
rotateAxis=(0, 1),
# angle=60,
border='w',
fill=(0, 0, 255, 100))
self.plot_item.addItem(text_item, ignoreBounds=True)
text_item.setPos(x_pos, 0)
self.event_texts.append(text_item)
elif checked_state == 0:
for event_vline in self.event_vlines:
self.plot_item.removeItem(event_vline)
for text_item in self.event_texts:
self.plot_item.removeItem(text_item)
def __mouse_clicked__(self, event: MouseClickEvent) -> None:
"""Function triggered when the user clicks on the plot. Display a vertical line under the mouse click and call
function ``__update_xy_original__``
Parameters
----------
event: pyqtgraph.MouseClickEvent
"""
pos = event.scenePos()
mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos)
instant = datetime.datetime.fromtimestamp(mouse_point.x(), tz=datetime.timezone.utc)
self.measure_vline.setPos(mouse_point.x())
self.__update_xy_original__(instant)
def __mouse_moved__(self, pos: QPointF) -> None:
"""Function triggered when the user's mouse cursor hovers over the plot. Display a vertical line where the
cursor is, and update time shift with click-fixed line, if any.
Parameters
----------
pos: PyQt5.QtCore.QPointF
"""
mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos)
self.cursor_vline.setPos(mouse_point.x())
if self.measure_vline.getPos() != [0, 0]:
timeshift_s = abs(self.measure_vline.getPos()[0] - self.cursor_vline.getPos()[0])
self.main_ui.explo_label_timeshift.setText("Time shift: " + "{:.2f}".format(timeshift_s) + "s")
def __get_timeseries_x_values__(self, timeseries: pd.DataFrame, timeshift_sec: float = 0) -> list:
# As it is a _step_ curve, add a last datetime point to determine the end of the last step. This is the datetime
# 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)
# Convert to epoch
x_values = utils.pd_time_to_epoch_ms(x_values)
return x_values
def __get_timeseries_y_values__(self, timeseries: pd.DataFrame, offset: float = 0.0, mult: float = 1.0) -> list:
# Get original value if it is a numeric, otherwise get its coded integer version.
if is_numeric_dtype(timeseries["value"]):
y_values = list(timeseries["value"])
else:
y_values = list(timeseries["value_int"])
# 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]
return y_values
####################################################################################################################
# Save/load current setup
def __check_setup_save_name__(self, filename: str):
valid, error_msg = self.current_dataset.setup_filename_is_valid(filename)
if not valid:
self.main_ui.statusbar.showMessage(error_msg, 5000)
self.main_ui.explo_lineedit_setup_name.setStyleSheet("color: 'red';")
self.main_ui.explo_pushbutton_setup_save.setEnabled(False)
else:
self.main_ui.explo_pushbutton_setup_save.setEnabled(True)
self.main_ui.explo_lineedit_setup_name.setStyleSheet("color: 'black';")
def __save_setup__(self):
# Dataset
dataset = self.current_dataset
# File name
filename = self.main_ui.explo_lineedit_setup_name.text()
# Variables
variable_df = pd.DataFrame()
table = self.main_ui.explo_tablewidget_variables
for row_id in range(table.rowCount()):
row_dict = {"instrument": table.cellWidget(row_id, self.INSTRUMENT_COL).currentText(),
"variable": table.cellWidget(row_id, self.VARIABLE_COL).currentText(),
"color": table.item(row_id, self.COLOR_COL).background().color().name(),
"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(),
"visible": table.cellWidget(row_id, self.VISIBLE_COL).checkState() == 2}
variable_df = variable_df.append(row_dict, ignore_index=True)
# View range
view_range = self.plot_item.getViewBox().viewRange()
dataset.save_setup(filename, variable_df, view_range)
self.__refresh_existing_setups__()
# Reset filename input widgets
self.main_ui.explo_lineedit_setup_name.setText(None)
self.main_ui.explo_pushbutton_setup_save.setEnabled(False)
def __load_setup__(self):
# Clear table and plot
self.main_ui.explo_tablewidget_variables.setRowCount(0)
self.__initialize_plot__()
# Get a dataframe containing the variables data
filename = self.main_ui.explo_listwidget_setup_list.selectedItems()[0].text()
variable_df, view_range_dict = self.current_dataset.load_setup(filename)
# Variables: table (and automatically: plot)
table = self.main_ui.explo_tablewidget_variables
for row_id, row in variable_df.iterrows():
self.__add_new_row_in_variable_table__()
# Instrument
instrument_index = table.cellWidget(row_id, self.INSTRUMENT_COL).findText(row["instrument"])
table.cellWidget(row_id, self.INSTRUMENT_COL).setCurrentIndex(instrument_index)
# Variable
variable_index = table.cellWidget(row_id, self.VARIABLE_COL).findText(row["variable"])
table.cellWidget(row_id, self.VARIABLE_COL).setCurrentIndex(variable_index)
# Color
color = QColor(row["color"])
table.item(row_id, self.COLOR_COL).setBackground(color)
# Offset
table.cellWidget(row_id, self.OFFSET_COL).setValue(row["offset"])
# Mult
table.cellWidget(row_id, self.MULT_COL).setValue(row["mult"])
# Timeshift
table.cellWidget(row_id, self.TIMESHIFT_COL).setValue(row["timeshift"])
# Visible
table.cellWidget(row_id, self.VISIBLE_COL).setChecked(row["visible"])
self.__apply_variable_change__(row_id) # Useful only for color change
# Plot view range
self.plot_item.getViewBox().enableAutoRange(enable=False)
# self.plot_item.getViewBox().setYRange(view_range_dict["ymin"], view_range_dict["ymax"], padding=0)
# self.plot_item.getViewBox().setXRange(view_range_dict["xmin"], view_range_dict["xmax"], padding=0)
self.plot_item.getViewBox().setRange(xRange=(view_range_dict["xmin"], view_range_dict["xmax"]),
yRange=(view_range_dict["ymin"], view_range_dict["ymax"]),
padding=0)
def __refresh_existing_setups__(self):
files = self.current_dataset.get_setup_saved_files()
self.main_ui.explo_listwidget_setup_list.clear()
for file in files:
self.main_ui.explo_listwidget_setup_list.addItem(file)
####################################################################################################################
# Stabilization analysis
def __init_stab__(self):
var_combo = self.stab_window.ui.stab_combobox_variable
self.stab_window.ui.stab_combobox_instrument.currentTextChanged.connect(
lambda inst_text, var_combo=var_combo: self.__update_variables_combobox__(inst_text, var_combo))
self.stab_window.ui.stab_combobox_variable.currentTextChanged.connect(self.__update_stab_plot__)
def __show_stab_window__(self):
self.__init_stab_plot__()
self.__update_instruments_combobox__(self.stab_window.ui.stab_combobox_instrument)
self.stab_window.show()
def __init_stab_plot__(self):
self.stab_plot_item = pg.PlotItem(axisItems={'bottom': utils.TimeAxisItem(orientation='bottom')})
self.stab_step_curve = pg.PlotCurveItem()
self.stab_plot_item.addItem(self.stab_step_curve)
self.stab_window.ui.stab_graphicsview.setCentralItem(self.stab_plot_item)
# Left
self.stab_region_left = pg.LinearRegionItem(brush=pg.mkBrush(color=[0, 0, 255, 40]))
self.stab_region_left.setZValue(10)
self.stab_plot_item.addItem(self.stab_region_left, ignoreBounds=True)
self.stab_region_left.sigRegionChanged.connect(lambda region: self.__update_stab_mean__(region, "left"))
self.stab_left_mean = 0.0
self.stab_left_mean_line = pg.InfiniteLine(pen=pg.mkPen(color=[0, 0, 255]), angle=0)
self.stab_plot_item.addItem(self.stab_left_mean_line)
# Right
self.stab_region_right = pg.LinearRegionItem(brush=pg.mkBrush(color=[0, 255, 0, 40]))
self.stab_region_right.setZValue(10)
self.stab_plot_item.addItem(self.stab_region_right, ignoreBounds=True)
self.stab_region_right.sigRegionChanged.connect(lambda region: self.__update_stab_mean__(region, "right"))
self.stab_right_mean = 0.0
self.stab_right_mean_line = pg.InfiniteLine(pen=pg.mkPen(color=[0, 255, 0]), angle=0)
self.stab_plot_item.addItem(self.stab_right_mean_line)
# High
self.stab_high_hline = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=0)
self.stab_plot_item.addItem(self.stab_high_hline)
self.stab_high_vline = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=90)
self.stab_plot_item.addItem(self.stab_high_vline)
# Low
self.stab_low_hline = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=0)
self.stab_plot_item.addItem(self.stab_low_hline)
self.stab_low_vline = pg.InfiniteLine(pen=pg.mkPen(color=[255, 0, 0], style=Qt.DotLine), angle=90)
self.stab_plot_item.addItem(self.stab_low_vline)
# Time delta text
self.stab_time_text = pg.TextItem(anchor=(0.5,1))
self.stab_plot_item.addItem(self.stab_time_text)
self.stab_timeseries = pd.DataFrame()
def __update_stab_plot__(self, var_combobox_text: str):
# 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 var_combobox_text == "":
return
# Get data
instrument_log = self.current_dataset.instlogs[self.stab_window.ui.stab_combobox_instrument.currentText()]
variable_name = self.stab_window.ui.stab_combobox_variable.currentText()
self.stab_timeseries = instrument_log.get_timeseries(variable_name).copy()
# Set data to step curve
x_values = self.__get_timeseries_x_values__(self.stab_timeseries)
self.stab_step_curve.setData(x=x_values,
y=self.__get_timeseries_y_values__(self.stab_timeseries),
stepMode=True)
# Convert data in a form more convenient for plot
self.stab_timeseries["datetime"] = utils.pd_time_to_epoch_ms(self.stab_timeseries["datetime"])
if not is_numeric_dtype(self.stab_timeseries["value"]):
self.stab_timeseries["value"] = self.stab_timeseries["value_int"]
# Get the main windows view box
main_range = self.plot_item.getViewBox().viewRange()
# Place the 2 (right/left) regions
x_min = main_range[0][0]
x_max = main_range[0][1]
x_1_third = x_min + ((x_max - x_min) / 3)
x_2_third = x_min + 2 * ((x_max - x_min) / 3)
self.stab_region_left.setRegion([x_min, x_1_third])
self.stab_region_right.setRegion([x_2_third, x_max])
# Re-calculate the position of the left/right means
self.__update_stab_mean__(self.stab_region_left, "left")
self.__update_stab_mean__(self.stab_region_right, "right")
# X range is the same as main window's plot X range
self.stab_plot_item.getViewBox().setRange(xRange=(main_range[0][0], main_range[0][1]),
yRange=(main_range[1][0], main_range[1][1]),
padding=0)
def __update_stab_mean__(self, region: pg.LinearRegionItem, side: str):
df = self.stab_timeseries.copy()
# Get the selected region's boundaries
xmin = region.getRegion()[0]
xmax = region.getRegion()[1]
# Compute x_value's average over the selected region.
mean = df[(df["datetime"] >= xmin) & (df["datetime"] <= xmax)]["value"].mean()
if math.isnan(mean):
return
# Update global "mean" variable
setattr(self, "stab_" + side + "_mean", mean)
# Update 'mean' horizontal line position
line = getattr(self, "stab_" + side + "_mean_line")
line.setValue(mean)
self.__compute_stab_delta__()
def __compute_stab_delta__(self):
# Left
left_mean = self.stab_left_mean
right_mean = self.stab_right_mean
high_y = max(left_mean, right_mean)
low_y = min(left_mean, right_mean)
ten_prct = (high_y - low_y) * 0.1
high_minus_ten = high_y - ten_prct
low_plus_ten = low_y + ten_prct
# Find timeseries' value where crossing 'high_minus_ten' and 'low_plus_ten'
df = self.stab_timeseries.copy()
# Keep only the data between both regions.
df = df[(df["datetime"] > self.stab_region_left.getRegion()[1])
& (df["datetime"] < self.stab_region_right.getRegion()[0])
& (df["value"] < high_minus_ten)
& (df["value"] > low_plus_ten)]
if df.empty:
return
high_datetime = df.loc[df["value"].idxmax()]["datetime"]
low_datetime = df.loc[df["value"].idxmin()]["datetime"]
# Plot lines
self.stab_high_hline.setValue(high_minus_ten)
self.stab_low_hline.setValue(low_plus_ten)
self.stab_high_vline.setValue(high_datetime)
self.stab_low_vline.setValue(low_datetime)
# Add text
time_delta = high_datetime - low_datetime
self.stab_time_text.setText("<-- " + "{:.2f}".format(time_delta) + "s -->")
self.stab_time_text.setPos(low_datetime + (high_datetime-low_datetime)/2,
high_y)