def setDataFrame(self, dataFrame): self.df = dataFrame dataModel = DataFrameModel() dataModel.setDataFrame(self.df) self.dataModel = dataModel self.dataListView.setModel(dataModel) self.dataTableView.setViewModel(dataModel) self.dataComboBox.setModel(dataModel) # self.dataTableView.resizeColumnsToContents() # create a simple item model for our choosing combobox columnModel = QtGui.QStandardItemModel() for column in self.df.columns: columnModel.appendRow(QtGui.QStandardItem(column)) self.chooseColumnComboBox.setModel(columnModel) self.tableViewColumnDtypes.setModel(dataModel.columnDtypeModel()) self.tableViewColumnDtypes.horizontalHeader().setDefaultSectionSize( 200) self.tableViewColumnDtypes.setItemDelegateForColumn( 1, DtypeComboDelegate(self.tableViewColumnDtypes)) dataModel.changingDtypeFailed.connect(self.changeColumnValue)
def test_copyDataFrame(copy, operator): dataFrame = pandas.DataFrame([0], columns=["A"]) model = DataFrameModel(dataFrame, copyDataFrame=copy) assert operator(id(model.dataFrame()), id(dataFrame)) model.setDataFrame(dataFrame, copyDataFrame=copy) assert operator(id(model.dataFrame()), id(dataFrame))
def test_copyDataFrame(copy, operator): dataFrame = pandas.DataFrame([0], columns=['A']) model = DataFrameModel(dataFrame, copyDataFrame=copy) assert operator(id(model.dataFrame()), id(dataFrame)) model.setDataFrame(dataFrame, copyDataFrame=copy) assert operator(id(model.dataFrame()), id(dataFrame))
def setDataFrame(self, dataFrame): self.df = dataFrame dataModel = DataFrameModel() dataModel.setDataFrame(self.df) self.dataListView.setModel(dataModel) self.dataTableView.setViewModel(dataModel) self.dataComboBox.setModel(dataModel) for index, column in enumerate(dataModel.dataFrame().columns): dtype = dataModel.dataFrame()[column].dtype self.updateDelegates(index, dtype) #self.updateDelegates() # self.dataTableView.resizeColumnsToContents() # create a simple item model for our choosing combobox columnModel = QtGui.QStandardItemModel() for column in self.df.columns: columnModel.appendRow(QtGui.QStandardItem(column)) self.chooseColumnComboBox.setModel(columnModel) self.tableViewColumnDtypes.setModel(dataModel.columnDtypeModel()) self.tableViewColumnDtypes.horizontalHeader().setDefaultSectionSize(200) self.tableViewColumnDtypes.setItemDelegateForColumn(1, DtypeComboDelegate(self.tableViewColumnDtypes)) dataModel.dtypeChanged.connect(self.updateDelegates) dataModel.changingDtypeFailed.connect(self.changeColumnValue)
def test_setDataFrame(): dataFrame = pandas.DataFrame([0], columns=["A"]) model = DataFrameModel() model.setDataFrame(dataFrame) assert not model.dataFrame().empty assert model.dataFrame() is dataFrame with pytest.raises(TypeError) as excinfo: model.setDataFrame(None) assert "pandas.core.frame.DataFrame" in unicode(excinfo.value)
def test_setDataFrame(): dataFrame = pandas.DataFrame([0], columns=['A']) model = DataFrameModel() model.setDataFrame(dataFrame) assert not model.dataFrame().empty assert model.dataFrame() is dataFrame with pytest.raises(TypeError) as excinfo: model.setDataFrame(None) assert "pandas.core.frame.DataFrame" in unicode(excinfo.value)
def test_flags(): model = DataFrameModel(pandas.DataFrame([0], columns=["A"])) index = model.index(0, 0) assert index.isValid() assert model.flags(index) == Qt.ItemIsSelectable | Qt.ItemIsEnabled model.enableEditing(True) assert model.flags(index) == Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable model.setDataFrame(pandas.DataFrame([True], columns=["A"])) index = model.index(0, 0) model.enableEditing(True) assert model.flags(index) != Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable assert model.flags(index) == Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
def display_query(selcols): ## setup a new empty model model1 = DataFrameModel() ## setup an application and create a table view widget app = QtGui.QApplication([]) widget1 = DataTableWidget() widget1.resize(1600, 800) widget1.show() ## asign the created model widget1.setViewModel(model1) ## fill the model with data model1.setDataFrame(selcols) ## start the app""" app.exec_() return
def test_flags(): model = DataFrameModel(pandas.DataFrame([0], columns=['A'])) index = model.index(0, 0) assert index.isValid() assert model.flags(index) == Qt.ItemIsSelectable | Qt.ItemIsEnabled model.enableEditing(True) assert model.flags( index) == Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable model.setDataFrame(pandas.DataFrame([True], columns=['A'])) index = model.index(0, 0) model.enableEditing(True) assert model.flags( index) != Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable assert model.flags( index ) == Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
# use QtGui from the compat module to take care if correct sip version, etc. from pandasqt.compat import QtGui from pandasqt.models.DataFrameModel import DataFrameModel from pandasqt.views.DataTableView import DataTableWidget from pandasqt.views._ui import icons_rc """setup a new empty model""" model = DataFrameModel() """setup an application and create a table view widget""" app = QtGui.QApplication([]) widget = DataTableWidget() widget.resize(800, 600) widget.show() """asign the created model""" widget.setViewModel(model) """create some test data""" data = { 'A': [10, 11, 12], 'B': [20, 21, 22], 'C': ['Peter Pan', 'Cpt. Hook', 'Tinkerbell'] } df = pandas.DataFrame(data) """convert the column to the numpy.int8 datatype to test the delegates in the table int8 is limited to -128-127 """ df['A'] = df['A'].astype(numpy.int8) df['B'] = df['B'].astype(numpy.float16) """fill the model with data""" model.setDataFrame(df) """start the app""" app.exec_()
from pandasqt.views._ui import icons_rc """setup a new empty model""" model = DataFrameModel() """setup an application and create a table view widget""" app = QtGui.QApplication([]) widget = DataTableWidget() widget.resize(800, 600) widget.show() """asign the created model""" widget.setViewModel(model) """create some test data""" data = { 'A': [10, 11, 12], 'B': [20, 21, 22], 'C': ['Peter Pan', 'Cpt. Hook', 'Tinkerbell'] } df = pandas.DataFrame(data) """convert the column to the numpy.int8 datatype to test the delegates in the table int8 is limited to -128-127 """ df['A'] = df['A'].astype(numpy.int8) df['B'] = df['B'].astype(numpy.float16) """fill the model with data""" model.setDataFrame(df) """start the app""" app.exec_()
class TracksterGUI(Ui_MainWindow): # ----- # Setup # ----- # PyQt signals to be emitted when the underlying database is change # Lets components that hold views on that data to update themselves wl_data_changed = Signal() # Workout log changed bw_data_changed = Signal() # Body weight log changed lt_data_changed = Signal() # Lift types database changed build_data_changed = Signal() # The workout being built in the Build tab was changed log_data_changed = Signal() analysis_data_changed = Signal() def __init__(self): # super().__init__() self.db = Backend(filename) self._wl_model = DataFrameModel(self.db.workout_log, copyDataFrame=False, show_index=True) self._bw_model = DataFrameModel(self.db.weight_log, copyDataFrame=False, show_index=True) self._lt_model = DataFrameModel(self.db.lift_types, copyDataFrame=False, show_index=False) def setupUi(self, main_window): super().setupUi(main_window) self.setup_global() self.__setup_dashboard_tab() self.__setup_body_weight_tab() self.__setup_log_tab() self.__setup_build_tab() self.__setup_analysis_tab() self.__connect_update_slots_and_signals() def setup_global(self): self._wl_model.timestampFormat = "yyyy-MM-dd" self._bw_model.timestampFormat = "yyyy-MM-dd" self._lt_model.timestampFormat = "yyyy-MM-dd" self.actionSave.triggered.connect(self.save_logs) self.actionRevert.triggered.connect(self.revert_from_temp) self.tabs.tabBarClicked.connect(self.switching_main_tabs_event) def __setup_dashboard_tab(self): pass def __setup_body_weight_tab(self): self.bw_table.setModel(self._bw_model) self._bw_model.enableEditing(True) self._bw_model.set_cell_colors(Theme.bw_color_cols, Theme.red_brush, Theme.green_brush) self.bw_table.tableView.setSortingEnabled(False) self.bw_table.tableView.scrollToBottom() self.bw_table.enableEditing(True) self.bw_date.setDate(QDate.currentDate()) self.bw_plot_data() self.bw_update_stats() self.bw_add.clicked.connect(self.bw_add_entry) self.bw_save.clicked.connect(self.db.save_bw_temp) self.bw_delete.clicked.connect(self.bw_delete_row) def __setup_log_tab(self): self._log_table_view = pd.DataFrame() self._log_table_model = DataFrameModel(self._log_table_view, copyDataFrame=False, show_index=False) self._log_table_model.enableEditing(True) self._log_table_model.dataChanged.connect(self.log_values_changed) self.log_table.setModel(self._log_table_model) self.log_table.horizontalHeader().setStretchLastSection(True) self._log_date = pd.Timestamp.today() self._log_lift = '' self._log_unsynced_changes = False self._log_unsaved_changes = False self.log_date.setDate(QDate.currentDate()) self.log_cal.setSelectedDate(QDate.currentDate()) self.log_date_changed(pd.Timestamp.today()) self.log_populate_lifts_box() self.log_populate_table() self.log_date.dateChanged.connect(self.log_date_selection_changed) self.log_cal.selectionChanged.connect(self.log_cal_selection_changed) self.log_add.clicked.connect(self.log_add_entry) self.log_save.clicked.connect(self.log_sync_changes) self.log_delete.clicked.connect(self.log_del_row) def __setup_build_tab(self): self._build_cur_workout = pd.DataFrame() self._build_cur_workout_view = pd.DataFrame() self._build_cur_lift = None self._build_tgt_e1rm = -1 self._build_tabs = OrderedDict() self._build_unsaved_changes = False self._build_cur_model = DataFrameModel(self._build_cur_workout, copyDataFrame=False, show_index=False) self._build_pre_model = DataFrameModel(dataFrame=None, copyDataFrame=False, show_index=False) self._build_cur_model_view = DataFrameModel(dataFrame=None, copyDataFrame=False, show_index=False) self.build_pre_workout.setModel(self._build_pre_model) self._build_rpe_model = RpeTableModel(self.db) self.build_RPE.setModel(self._build_rpe_model) self._build_stats_model = StatsTableModel(self.db) self.build_stats.setModel(self._build_stats_model) self.build_date.setDate(QtCore.QDate.currentDate()) self.build_tree_populate() self.build_search.textChanged.connect(self.build_filter) self.build_lifts.itemDoubleClicked.connect(self.build_lift_selected) self.build_previous_slider.valueChanged.connect(self.build_populate_previous_workout_table) self.build_target.valueChanged.connect(self.build_populate_rpe_table) self.build_btn_add.clicked.connect(self.build_add_btn_clicked) self.build_RPE.clicked.connect(self.build_top_set_selected) self.build_tabs.currentChanged.connect(self.build_lift_tab_changed) self.build_accept.clicked.connect(self.build_accept_workout) self.build_clear.clicked.connect(self.build_clear_workout) self.build_btn_delete.clicked.connect(self.build_delete_row) def __setup_analysis_tab(self): self.an_data = {} self._an_cur_analysis = None self.an_type.setCurrentIndex(0) self.an_update_values_box() self.an_type.currentTextChanged.connect(self.an_update_values_box) self.an_plots.itemSelectionChanged.connect(self.an_analysis_selected) self.an_add.clicked.connect(self.an_add_analysis) self.an_remove.clicked.connect(self.an_remove_analysis) def __connect_update_slots_and_signals(self): self.bw_data_changed.connect(self._bw_model.layoutChanged.emit) self.bw_data_changed.connect(self.bw_plot_data) self.bw_data_changed.connect(self.bw_update_stats) self._bw_model.dataFrameChanged.connect(self.bw_data_changed.emit) self._bw_model.dataChanged.connect(self.bw_data_changed.emit) self.wl_data_changed.connect(self.log_data_changed.emit) self.lt_data_changed.connect(self.an_update_values_box) self.lt_data_changed.connect(self.build_tree_populate) self.lt_data_changed.connect(self.log_populate_lifts_box) self.build_data_changed.connect(self.build_update_previous_workout_stats) self.build_data_changed.connect(self.build_update_current_workout_stats) self.build_data_changed.connect(self.build_populate_previous_workout_table) self.build_data_changed.connect(self.build_gen_workout_tables) self.log_data_changed.connect(self.log_populate_table) self.log_data_changed.connect(self.log_populate_stats) self.log_data_changed.connect(partial(self.build_stats.model().update, db_modified=True)) self.analysis_data_changed.connect(self.an_plot_analysis) self.analysis_data_changed.connect(self.an_update_values_box) self.analysis_data_changed.connect(self.an_populate_plots_list) # ----- # Body Weight Tab # ----- def bw_add_entry(self): high = None low = None bf = None try: date = pd.Timestamp(self.bw_date.text()) if self.bw_high.text() != '': high = float(self.bw_high.text()) if self.bw_low.text() != '': low = float(self.bw_low.text()) if self.bw_bf.text() != '': bf = float(self.bw_bf.text()) / 100.0 comments = self.bw_comments.text() self.db.add_bodyweight_entry(date, high, low, bf, comments) except (ValueError, TypeError): pass self.bw_data_changed.emit() @pyqtSlot() def bw_plot_data(self): # TODO: Make prettier self.bw_plot.clear() above_goal_data = np.maximum(self.db.weight_log.High, self.db.weight_log.Goal) below_goal_data = np.minimum(self.db.weight_log.High, self.db.weight_log.Goal) self.bw_plot.plot_ts(self.db.weight_log.Goal) self.bw_plot.plot_ts(above_goal_data) self.bw_plot.plot_ts(below_goal_data) self.bw_plot.plot_ts(self.db.weight_log.Low) above_goal_fill = pg.FillBetweenItem(self.bw_plot.plotItem.curves[0], self.bw_plot.plotItem.curves[1], 'r') below_goal_fill = pg.FillBetweenItem(self.bw_plot.plotItem.curves[0], self.bw_plot.plotItem.curves[2], 'g') self.bw_plot.getPlotItem().addItem(above_goal_fill) self.bw_plot.getPlotItem().addItem(below_goal_fill) ma_data = pd.rolling_mean(self.db.weight_log.High, 7, min_periods=1) self.bw_plot.plot_ts(ma_data) @pyqtSlot() def bw_update_stats(self): daily_diff = self.db.weight_log.Diff[self.db.weight_log.Diff.notnull()].iloc[-1] daily_diff = round(daily_diff, 1) self.bw_daily_diff.setText(str(daily_diff)) goal_diff = self.db.weight_log.Goal_Diff[self.db.weight_log.Goal_Diff.notnull()].iloc[-1] goal_diff = round(goal_diff, 1) self.bw_goal_diff.setText(str(goal_diff)) weekly_diff = self.db.weight_log.High.iloc[-7:].mean() - self.db.weight_log.High.iloc[-14:-7].mean() weekly_diff = round(weekly_diff, 1) self.bw_avg_week.setText(str(weekly_diff)) @pyqtSlot() def bw_delete_row(self): # self.bw_table.removeRow(triggered=True) # self.bw_data_changed.emit() pass # ----- # Analysis Tab # ----- def an_add_analysis(self): data = self.db.workout_log group = self.an_type.currentText() val = self.an_value.currentText() try: if group == 'Slot': data = data[data.Slot == val] elif group == 'Category': data = data[data.Category == val] elif group == 'Lift': data = data[data.Lift == val] else: return except (ValueError, TypeError) as err: log_err(err, "Invalid analysis type selected: {}, {}".format(group, val)) analysis_funcs = {'E1RMs': analysis.get_max_e1RM, 'Volume': analysis.get_volume, 'Norm V': analysis.calc_normalized_tonnage, 'Fatigue': analysis.get_fatigue} selected_func = analysis_funcs[self.an_analysis.currentText()] analyzed_data = selected_func(data) title = '{}: {} -> {}'.format(self.an_analysis.currentText(), group, val) if self.an_mov_avg.isChecked(): period = self.an_period.value() if 'e1rm' in selected_func.__name__: analyzed_data = analyzed_data.resample('1D', label='right', how=np.max) elif 'vol' in selected_func.__name__ or 'fatigue' in selected_func.__name__: analyzed_data = analyzed_data.resample('1D', label='right', how=np.sum) analyzed_data = analyzed_data.fillna(0) analyzed_data = pd.rolling_mean(analyzed_data, period, min_periods=1) if 'fatigue' in selected_func.__name__: analyzed_data *= 7.0 title += ' ({} day avg)'.format(period) self.an_data[title] = analyzed_data self.analysis_data_changed.emit() @pyqtSlot() def an_populate_plots_list(self): self.an_plots.clear() titles = [x for x in self.an_data.keys()] self.an_plots.addItems(titles) @pyqtSlot() def an_analysis_selected(self, which=None): if not which or not isinstance(which, str): which = self.an_plots.currentItem().text() self._an_cur_analysis = which self.an_remove.setEnabled(True) @pyqtSlot() def an_plot_analysis(self): self.an_plot.plot_with_shared_axes(self.an_data) @pyqtSlot() def an_remove_analysis(self, which): if which: if isinstance(which, QTreeWidgetItem): which = which.text(0) elif isinstance(which, str): pass else: return elif pd.notnull(self._an_cur_analysis): which = self._an_cur_analysis if which in self.an_data: self.an_data.pop(which) self.analysis_data_changed.emit() if len(self.an_data) > 1: self.an_remove.setEnabled(False) @pyqtSlot(str, str) def an_update_values_box(self): self.an_value.clear() type = self.an_type.currentText() vals = self.db.lift_types[type].unique() self.an_value.addItems(vals) # ----- # Log Tab # ----- @pyqtSlot() def log_populate_table(self, date=None, lift=None): if lift and date and isinstance(date, pd.Timestamp) and isinstance(lift, str): if lift == self._log_cur_lift and date == self._log_date: return else: self._log_cur_lift = lift self._log_date = date self._log_table_view = self.db.workout_log[self.db.workout_log.index == self._log_date].reset_index() self._log_table_view = self._log_table_view[['Slot', 'Category', 'Lift', 'Weight', 'Reps', 'RPE', 'Comments']] self._log_table_model.setDataFrame(self._log_table_view) self._log_table_model.dataChanged.emit() self._log_table_model.layoutChanged.emit() @pyqtSlot() def log_populate_stats(self, lift=None): if pd.isnull(self.log_date) or self._log_table_view.empty: return self.log_tree.clear() cur_wkt = self.db.workout_log[self.db.workout_log.index == self._log_date] pre_30d = self.db.workout_log[self.db.workout_log.index >= self._log_date - pd.Timedelta(days=30)] pre_30d = pre_30d[pre_30d.index < self._log_date] e1RM_node = QTreeWidgetItem() e1RM_node.setText(0, 'e1RMs:') for lift, lift_data in cur_wkt.groupby('Lift'): e1RM = max(analysis.get_max_e1RM(lift_data)) if pre_30d.Lift.isin([lift]).any(): e1RM_pre_30d = max(analysis.get_max_e1RM(pre_30d[pre_30d.Lift == lift])) else: e1RM_pre_30d = '' child_node = QTreeWidgetItem() child_node.setText(0, str(lift)) child_node.setText(1, str(e1RM)) child_node.setText(2, str(e1RM_pre_30d)) e1RM_node.addChild(child_node) volume_node = QTreeWidgetItem() volume_node.setText(0, 'Volume:') for cat, cat_data in cur_wkt.groupby('Category'): vol = sum(cat_data.Volume) if pre_30d.Category.isin([cat]).any(): vol_pre_30d = round(pre_30d[pre_30d.Category == cat].resample('1d', how=np.sum).Volume.mean()) else: vol_pre_30d = '' child_node = QTreeWidgetItem() child_node.setText(0, str(cat)) child_node.setText(1, str(vol)) child_node.setText(2, str(vol_pre_30d)) volume_node.addChild(child_node) nt_node = QTreeWidgetItem() nt_node.setText(0, 'Normalized Tonnage:') for cat, cat_data in cur_wkt.groupby('Category'): nt = round(analysis.calc_normalized_tonnage(cat_data).sum()) if pre_30d.Category.isin([cat]).any(): nt_pre_30d = round(analysis.calc_normalized_tonnage( pre_30d[pre_30d.Category == cat]).resample('1d', how=np.sum).mean()) else: nt_pre_30d = '' child_node = QTreeWidgetItem() child_node.setText(0, str(cat)) child_node.setText(1, str(nt)) child_node.setText(2, str(nt_pre_30d)) nt_node.addChild(child_node) # TODO: Figure out why fatigue isn't being normalized to weekly average fatigue_node = QTreeWidgetItem() fatigue_node.setText(0, 'Fatigue:') for slot, slot_data in cur_wkt.groupby('Slot'): fatigue = analysis.get_fatigue(slot_data) fatigue = fatigue.resample('1d', how=np.sum) fatigue = fatigue.fillna(0).iloc[-7:].sum() if pre_30d.Slot.isin([slot]).any(): fatigue_30d = analysis.get_fatigue(pre_30d[pre_30d.Slot == slot]) fatigue_30d = fatigue_30d.resample('1d', how=np.sum) fatigue_30d = round(fatigue_30d.fillna(0).mean() * 7.0, 1) else: fatigue_30d = '' child_node = QTreeWidgetItem() child_node.setText(0, str(slot)) child_node.setText(1, str(fatigue)) child_node.setText(2, str(fatigue_30d)) fatigue_node.addChild(child_node) self.log_tree.addTopLevelItems([e1RM_node, volume_node, fatigue_node, nt_node]) self.log_tree.expandAll() @pyqtSlot() def log_add_entry(self): try: date = pd.Timestamp(self.log_date.date().toPyDate()) lift = self.log_lift.currentText() if not self.db.is_valid_lift(lift): raise ValueError('{} is not a valid lift'.format(lift)) raw_sets = self.log_sets.text().split(',') parsed_sets = [] for set in raw_sets: parsed_sets.extend(analysis.parse_sets(set.strip())) comments = self.log_comments.text() for set in parsed_sets: weight, reps, rpe = set self.db.add_set(date, lift, weight, reps, rpe, comments) self.db.calc_workout_data() self.log_date_changed(date - pd.Timedelta(days=1)) self.log_date_changed(date) self.wl_data_changed.emit() self.log_lift.setCurrentText('') self.log_sets.setText('') except (ValueError, TypeError) as err: log_err(err, "Couldn't parse log entry") @pyqtSlot() def log_del_row(self): selection = self.log_table.selectedIndexes() rows = [index.row() for index in selection] # TODO: add enable/disable toggle for delete button based on selection status if rows and len(rows) > 0: abs_rows = [self.log_get_absolute_index(self._log_date, rel_row) for rel_row in rows] self.db.workout_log.reset_index(inplace=True) self.db.workout_log.drop(self.db.workout_log.index[abs_rows], inplace=True) self.db.workout_log.set_index('Date', inplace=True) self.log_table.model().removeDataFrameRows(rows) self._log_unsynced_changes = True self._log_unsaved_changes = True @pyqtSlot() def log_values_changed(self): table_data = self._log_table_model.dataFrame() idx = table_data.index abs_idx = self.log_get_absolute_index(self._log_date, idx) if not table_data.empty: filtered_master = self.db.workout_log.iloc[abs_idx].reset_index()[table_data.keys()] if not table_data.equals(filtered_master): self._log_unsynced_changes = True self._log_unsaved_changes = True return True else: return False else: return False @pyqtSlot() def log_sync_changes(self, dont_prompt=False): if self._log_unsynced_changes and (self.prompt_save_changes() or dont_prompt): new_data = self._log_table_model.dataFrame() self.db.merge_workout_data(new_data, self._log_date) self._log_unsynced_changes = False @pyqtSlot() def log_save_changes(self): if self._log_unsaved_changes and self.dialog_save(): if self._log_unsynced_changes: self.log_sync_changes(dont_prompt=True) self.db.save_to_disk() self._log_unsaved_changes = False @pyqtSlot() def log_import(self): pass @pyqtSlot() def log_date_changed(self, date): if self._log_unsynced_changes: self.log_sync_changes() if isinstance(date, pd.Timestamp) and pd.notnull(date) and self._log_date != date: self._log_date = date if self.log_date.date().toPyDate() != date.to_pydatetime(): self.log_date.setDate(QDate(date.to_datetime())) if self.log_cal.selectedDate().toPyDate() != date.to_pydatetime(): self.log_cal.setSelectedDate(QDate(date.to_datetime())) self.log_data_changed.emit() @pyqtSlot() def log_date_selection_changed(self): try: date = pd.Timestamp(self.log_date.date().toPyDate()) self.log_date_changed(date) except (ValueError, TypeError) as err: log_err(err, "Couldn't convert log_date's value to a date... skipping") @pyqtSlot() def log_cal_selection_changed(self): try: date = pd.Timestamp(self.log_cal.selectedDate().toPyDate()) self.log_date_changed(date) except (ValueError, TypeError) as err: log_err(err, "Couldn't convert log_cal's value to a date... skipping") @pyqtSlot() def log_populate_lifts_box(self): lifts = [str(lift) for lift in self.db.lift_types.Lift] self.log_lift.addItems(lifts) def log_get_absolute_index(self, date, offset): dat = self.db.workout_log.reset_index() dat = dat[dat.Date == date] if not dat.empty: return dat.index[offset] else: return [] @pyqtSlot() def log_tab_gain_focus(self): self.log_data_changed.emit() @pyqtSlot() def log_tab_lose_focus(self): self.log_save_changes() # ----- # Build Tab # ----- # TODO: recalc e1RM in log table after value changed, also recalc curr workout stats # TODO: e1RM Calculator button # TODO: Clear button only clears current build_tab and deletes tab # TODO: test/implement delete button # TODO: Quick add button def build_filter(self): filter_string = self.build_search.text() if not filter_string: filter_string = '' matching_indices = self.db.lift_types.Lift.map(lambda val: filter_string.lower() in val.lower()) dat = self.db.lift_types[matching_indices] self.build_tree_populate(dat) @pyqtSlot() def build_tree_populate(self, data=None): self.build_lifts.clear() if not isinstance(data, pd.DataFrame): data = self.db.lift_types dat = data.reset_index() root_children = [] for cat, cat_group in dat.groupby('Category', as_index=True, sort=False): root_child = QTreeWidgetItem() root_child.setText(0, cat) root_child.setBackground(0, Theme.gray_brush) root_child.setBackground(1, Theme.gray_brush) lift_children = [] for lift, _ in cat_group.groupby('Lift'): lift_child = QTreeWidgetItem() lift_child.setText(1, lift) lift_children.append(lift_child) root_child.insertChildren(1, lift_children) root_children.append(root_child) self.build_lifts.setColumnCount(2) self.build_lifts.insertTopLevelItems(0, root_children) self.build_lifts.expandAll() @pyqtSlot(str) def build_lift_selected(self, lift=None): # TODO: Clean this mess up and make it pretty # TODO: Clear current workout stats if lift: if isinstance(lift, QtWidgets.QTreeWidgetItem): txt = lift.text(1) elif isinstance(lift, str): txt = lift else: txt = '' else: txt = self.build_lifts.currentItem().text(1) if txt: if txt == self._build_cur_lift: return else: self._build_cur_lift = txt else: return pre_workouts = analysis.get_max_e1RM(self.db.workout_log[self.db.workout_log.Lift == self._build_cur_lift]) pre_e1RM = pre_workouts[pre_workouts.notnull()][-1] self._build_tgt_e1rm = pre_e1RM self.build_populate_rpe_table() self.build_target.setValue(int(self._build_tgt_e1rm)) self.build_btn_add.setEnabled(True) self.build_workout_switch_tables() self.build_data_changed.emit() @pyqtSlot(int) def build_populate_rpe_table(self, tgt_e1rm=None): if tgt_e1rm: if not isinstance(tgt_e1rm, Number): if issubclass(tgt_e1rm, QtWidgets.QWidget): tgt_e1rm = tgt_e1rm.text() tgt_e1rm = int(tgt_e1rm) if self._build_tgt_e1rm == tgt_e1rm: return self._build_tgt_e1rm = tgt_e1rm elif self._build_tgt_e1rm: tgt_e1rm = self._build_tgt_e1rm else: tgt_e1rm = None self.build_RPE.model().update(e1rm=tgt_e1rm) self.build_RPE.model().layoutChanged.emit() @pyqtSlot(int) def build_populate_previous_workout_table(self, lookback=None): # TODO: Concatenate current workout onto previous ones to allow more accurate stats if not self._build_cur_lift or not self.db.is_valid_lift(self._build_cur_lift): return elif not lookback: lookback = self.build_previous_slider.maximum() lift_workouts = self.db.workout_log[self.db.workout_log.Lift == self._build_cur_lift] idx_lookback = -1 * (int(self.build_previous_slider.maximum()) + 1) + lookback date_idx = sorted(lift_workouts.index.unique())[idx_lookback] workout = lift_workouts[lift_workouts.index == date_idx].reset_index() workout = workout[['Lift', 'Weight', 'Reps', 'RPE', 'e1RM']] self._build_pre_model.setDataFrame(workout, copyDataFrame=False) self.build_pre_workout.scrollToBottom() @pyqtSlot() def build_update_current_workout_stats(self): # TODO: Make this reflect changes that are manually entered into the workout table if self._build_cur_workout.empty or not self._build_cur_lift or not self.db.is_valid_lift(self._build_cur_lift): self.build_stats.model().clear(what='current') else: self.build_stats.model().update(self._build_cur_workout, self._build_cur_lift) @pyqtSlot() def build_update_previous_workout_stats(self): if not self._build_cur_lift or not self.db.is_valid_lift(self._build_cur_lift): self.build_stats.model().clear(what='all') else: self.build_stats.model().update(cur_lift=self._build_cur_lift) @pyqtSlot() def build_top_set_selected(self, index): if not index.isValid(): return try: top_set = index.data().split('(')[0].strip() self.build_top_set.setText(top_set) except (ValueError, AttributeError): pass @pyqtSlot() def build_add_btn_clicked(self): date = pd.Timestamp(self.build_date.text()) sets = [] # Warmup sets if self.build_warmup.text() == '': # Auto-calculate warmup if no text present warmup = analysis.calc_warmup(self._build_tgt_e1rm, self._build_cur_lift) sets.extend(warmup) else: for set in self.build_warmup.text().split(','): parsed = analysis.parse_sets(set.strip(), self._build_tgt_e1rm) sets.extend(parsed) # Main/Drop sets if self.build_top_set.text() != '': top_set = analysis.parse_sets(self.build_top_set.text()) sets.extend(top_set) top_weight, top_reps, top_rpe = top_set[-1] top_e1rm = analysis.calc_e1RM(self._build_cur_lift, top_weight, top_reps, top_rpe) fatigue = self.build_fatigue.value() self._build_cur_workout = self.db.make_workout(self._build_cur_lift, sets, date) drop_sets = self.build_get_drop_sets(top_set[-1], top_e1rm, fatigue, date) self._build_cur_workout = pd.concat((self._build_cur_workout, drop_sets), ignore_index=True) # Perform housekeeping self.build_clear.setEnabled(True) self.build_data_changed.emit() self.build_accept.setEnabled(True) self._log_unsaved_changes = True def build_get_drop_sets(self, top_set, e1RM, fatigue, date): dialog = QtWidgets.QDialog() dialog.ui = DialogDrop(self.db, top_set, e1RM, date, self._build_cur_lift, fatigue, self._build_cur_workout) dialog.ui.setupUi(dialog) dialog.exec_() dialog.show() if bool(dialog.result()): return dialog.ui.drop_sets else: return pd.DataFrame() def build_gen_workout_tables(self): if self._build_cur_workout.empty: return for lift in self._build_cur_workout.Lift.unique(): if lift not in self._build_tabs: if len(self._build_tabs) == 0: self.build_tabs.removeTab(0) tab = QtWidgets.QWidget() tab.setObjectName(lift) self.build_tabs.addTab(tab, lift) tab_table = QtWidgets.QTableView() self._build_cur_model_view.setDataFrame(self._build_cur_workout_view) tab_table.setModel(self._build_cur_model_view) tab_table.horizontalHeader().setDefaultSectionSize(93) tab_table.clicked.connect(self.build_workout_cell_clicked) layout = QtWidgets.QVBoxLayout(tab) layout.addWidget(tab_table) self._build_tabs[lift] = tab_table self.build_workout_switch_tables() @pyqtSlot() def build_workout_switch_tables(self): if self._build_cur_workout.empty or len(self._build_tabs) == 0: return lift_name_tab = -1 for lift_name in self._build_tabs.keys(): lift_name_tab += 1 if lift_name == self._build_cur_lift: break if lift_name_tab == -1: return self.build_tabs.setCurrentIndex(lift_name_tab) if self._build_cur_lift in self._build_tabs: try: model = self._build_tabs[self._build_cur_lift].model() self.build_save_changes(model.dataFrame()) self._build_cur_workout_view = self._build_cur_workout[self._build_cur_workout.Lift == self._build_cur_lift] self._build_cur_workout_view = self._build_cur_workout_view[['Lift', 'Weight', 'Reps', 'RPE', 'e1RM']] model.setDataFrame(self._build_cur_workout_view) model.enableEditing(True) model.dataChanged.emit() model.layoutChanged.emit() except Exception as err: log_err(err, "Couldn't emit datachanged signals to table model for {}".format(self._build_cur_lift)) @pyqtSlot() def build_lift_tab_changed(self, name): if self.build_tabs.currentWidget(): try: lift_name = self.build_tabs.currentWidget().objectName() self.build_lift_selected(lift_name) except Exception: pass @pyqtSlot() def build_workout_cell_clicked(self): self.build_btn_delete.setEnabled(True) # TODO: Tap dataChanged to update e1RM col and workout stats @pyqtSlot() def build_save_changes(self, data): if not data.empty: lift = data.Lift.unique()[0] self._build_cur_workout = self._build_cur_workout[self._build_cur_workout.Lift != lift] for row in self._build_cur_workout_view.itertuples(): new_set = self.db.make_set(lift, row.Weight, row.Reps, self._build_cur_workout.Date.iloc[0], row.RPE) self._build_cur_workout = self._build_cur_workout.append(new_set, ignore_index=True) @pyqtSlot() def build_accept_workout(self): if not self._build_cur_workout.empty: self._build_cur_workout = self._build_cur_workout.set_index('Date') self.db.workout_log = self.db.workout_log.append(self._build_cur_workout) self.build_clear_workout() self.wl_data_changed.emit() @pyqtSlot() def build_delete_row(self): curr_lift_table = self._build_tabs[self._build_cur_lift] rows = [index.row for index in curr_lift_table.currentSelection()] self._build_cur_model_view.removeDataFrameRows(rows) self.build_btn_delete.setEnabled(False) @pyqtSlot() def build_clear_workout(self): self._build_cur_lift = None self._build_cur_workout = pd.DataFrame() self.build_tabs.clear() self._build_tabs.clear() self.build_accept.setEnabled(False) self.build_clear.setEnabled(False) self.build_stats.model().clear(what='all') self.build_RPE.model().clear() self.build_pre_workout.model().dataChanged.emit() self.build_pre_workout.model().layoutChanged.emit() self.build_data_changed.emit() # ----- # Administrative # ----- @pyqtSlot(int) def switching_main_tabs_event(self, new_idx=None): old_idx = self.tabs.currentIndex() self.main_tab_losing_focus_event(old_idx) self.main_tab_gaining_focus_event(new_idx) def main_tab_losing_focus_event(self, idx_of): if idx_of == -1: return elif idx_of == self.tabs.indexOf(self.dash): pass elif idx_of == self.tabs.indexOf(self.bw): pass elif idx_of == self.tabs.indexOf(self.analysis): pass elif idx_of == self.tabs.indexOf(self.workout_log): self.log_tab_lose_focus() elif idx_of == self.tabs.indexOf(self.builder): pass def main_tab_gaining_focus_event(self, idx_of): if idx_of == -1: return elif idx_of == self.tabs.indexOf(self.dash): pass elif idx_of == self.tabs.indexOf(self.bw): pass elif idx_of == self.tabs.indexOf(self.analysis): pass elif idx_of == self.tabs.indexOf(self.workout_log): self.log_tab_gain_focus() elif idx_of == self.tabs.indexOf(self.builder): pass @pyqtSlot() def prompt_save_changes(self, selected_tab=None): dialog = QtWidgets.QDialog() dialog.ui = DialogUnsaved() dialog.ui.setupUi(dialog) dialog.exec_() dialog.show() return bool(dialog.result()) def save_logs(self): print('Saving') do_save = self.dialog_save() if do_save: self.db.save_to_disk() self._log_unsynced_changes = False def revert_from_temp(self): do_revert, which_log, which_bak = self.dialog_revert() if do_revert: self.db.restore_from_temp(which_log, which_bak) @staticmethod def dialog_save(): dialog = QtWidgets.QDialog() dialog.ui = DialogSave() dialog.ui.setupUi(dialog) dialog.exec_() dialog.show() return bool(dialog.result()) def dialog_revert(self): dialog = QtWidgets.QDialog() dialog.ui = DialogRevert(dialog, self.db) dialog.exec_() dialog.show() which_log = dialog.ui.revert_to_log which_file = dialog.ui.revert_to_file return bool(dialog.result()), which_log, which_file