diff --git a/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py b/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py index 16c6826..9f6ea43 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py @@ -3,6 +3,7 @@ Data Viewer: Custom BEC widget to view data from scans. """ import sys +from datetime import datetime from functools import partial from bec_lib import bec_logger @@ -74,6 +75,7 @@ class DataViewer(BECWidget, QWidget): self.input.scan_sel.currentItemChanged_connect(self.scan_sel_changed) self.input.load_button.clicked_connect(self.load_dataset) + self.input.unload_button.clicked_connect(self.unload_all_datasets) @SafeSlot() def scan_sel_changed(self, *_, **kwargs): @@ -88,6 +90,31 @@ class DataViewer(BECWidget, QWidget): logger.info(file.decode()) self.viewer.load_files([file.decode()]) + @SafeSlot() + def unload_all_datasets(self, *_): + self.viewer.clear_files() + + def duration_string(self, start: str, end: str) -> str: + start_dt = datetime.fromisoformat(start) + end_dt = datetime.fromisoformat(end) + + seconds = abs(int((end_dt - start_dt).total_seconds())) + + days, remainder = divmod(seconds, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, _ = divmod(remainder, 60) + + parts = [] + + if days: + parts.append(f"{days}d") + if hours: + parts.append(f"{hours}h") + if minutes: + parts.append(f"{minutes}min") + + return " ".join(parts) if parts else "<1min" + @SafeSlot() def on_history_update(self, *_): self.history = [] @@ -96,15 +123,17 @@ class DataViewer(BECWidget, QWidget): # and no scan has finished yet. Limit the history to the latest 20 scans. max_scans = min(len(self.client.history), 20) for n in range(1, max_scans): # last scans, up to 20 - logger.info(self.client.history[-n].metadata) + # logger.info(self.client.history[-n].metadata["bec"]["status"]) + start_time = self.client.history[-n].metadata["start_time"] + end_time = self.client.history[-n].metadata["end_time"] + logger.info(type(start_time)) scan_data = self.client.history[-n].metadata["bec"] # logger.info(scan_data) scan_number = scan_data["scan_number"] scan_name = scan_data["scan_name"] comment = scan_data["metadata"]["user_metadata"]["comment"] sample_name = scan_data["metadata"]["user_metadata"]["sample_name"] - # start = scan_data[] - # end = scan_data[] + status = scan_data["status"] self.history.append( { "scan_number": scan_number, @@ -112,6 +141,9 @@ class DataViewer(BECWidget, QWidget): "comment": comment, "sample_name": sample_name, "file_components": scan_data["file_components"], + "start_time": start_time, + "end_time": end_time, + "status": status, } ) tags = [] @@ -120,8 +152,15 @@ class DataViewer(BECWidget, QWidget): tags.append((sample_name, "#4A10D9")) if comment != "": tags.append((comment, "#FA10D9")) + if status == "closed": + tags.append((status, "#1F7023")) + elif status == "halted": + tags.append((status, "#A33047")) + else: + tags.append((status, "#656365")) + tags.append((self.duration_string(start_time, end_time), "#656365")) self.input.scan_sel.addTaggedItem(label=str(scan_number), tags=tags) - # logger.info(f"Scan history: {self.history}") + logger.info(f"Scan history: {self.history}") class InputPanel(QWidget): @@ -135,9 +174,12 @@ class InputPanel(QWidget): # Scan selection self.scan_sel = ListWidget("scan_sel", "Scan", ["Si", "Rh", "Pt"]) self.load_button = Button(label_button="Load Dataset", enabled=True) + self.unload_button = Button(label_button="Unload all", enabled=True) # Assemble complete scan selection group - self.input_group = Group("Scan selection", [self.scan_sel, self.load_button]) + self.input_group = Group( + "Scan selection", [self.scan_sel, self.load_button, self.unload_button] + ) self._layout.addWidget(self.input_group) self._layout.addStretch() diff --git a/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py b/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py index 572a972..9d7b4f7 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/qt_widgets.py @@ -149,40 +149,3 @@ class TaggedListWidget(QListWidget): def currentTags(self) -> list: item = self.currentItem() return item.data(_UserRole) or [] if item else [] - - -# ── Demo ────────────────────────────────────────────────────────────────────── -if __name__ == "__main__": - app = QApplication(sys.argv) - app.setStyle("Fusion") - - window = QWidget() - window.setWindowTitle("TaggedListWidget demo") - window.resize(420, 320) - - layout = QVBoxLayout(window) - layout.setContentsMargins(24, 24, 24, 24) - layout.setSpacing(12) - - lst = TaggedListWidget() - lst.addTaggedItem("NumPy", tags=[("1.26", "#2563EB"), ("stable", "#16A34A")]) - lst.addTaggedItem("Pandas", tags=[("2.2", "#7C3AED"), ("deprecated", "#DC2626")]) - lst.addTaggedItem("PyTorch", tags=[("2.3", "#2563EB"), ("cuda", "#DC2626")]) - lst.addTaggedItem("scikit-learn", tags=[("1.4", "#0891B2"), ("stable", "#16A34A")]) - lst.addTaggedItem("Matplotlib", tags=[("3.8", "#D97706")]) - lst.addTaggedItem("Plain item — no tags") - layout.addWidget(lst) - - info = QLabel() - - def on_selection(): - label = lst.currentItem().text() if lst.currentItem() else "" - tags = lst.currentTags() - tag_str = " ".join(t for t, _ in tags) if tags else "none" - info.setText(f"Selected: {label} tags: {tag_str}") - - lst.currentItemChanged.connect(lambda *_: on_selection()) - layout.addWidget(info) - - window.show() - sys.exit(app.exec()) diff --git a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py index b975214..4a99291 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py @@ -18,6 +18,7 @@ from qtpy.QtWidgets import ( QMainWindow, QRadioButton, QSizePolicy, + QSlider, QSplitter, QTableWidget, QTableWidgetItem, @@ -55,7 +56,7 @@ pg.setConfigOptions( def _styled_plot_widget(**kwargs) -> pg.PlotWidget: """Return a PlotWidget already themed to match the dark palette.""" pw = pg.PlotWidget(**kwargs) - pw.setBackground(pg.mkColor(*PG_PLOT_BG)) + # pw.setBackground(pg.mkColor(*PG_PLOT_BG)) ax = pw.getPlotItem() for side in ("left", "bottom", "right", "top"): ax.getAxis(side).setPen(pg.mkPen(color=PG_GRID, width=1)) @@ -65,7 +66,7 @@ def _styled_plot_widget(**kwargs) -> pg.PlotWidget: # ── HDF5 Tree helpers ────────────────────────────────────────────────────── -TYPE_ICONS = {"group": "📂", "dataset": "📊"} +# TYPE_ICONS = {"group": "📂", "dataset": "📊"} def dtype_tag(ds): @@ -234,43 +235,78 @@ class DataPanel(QWidget): def _show_plot_1d(self, data): self._clear_stack() - flat = data.reshape(-1).astype( - np.float32 - ) # TODO: Works, but I would prefer 1d plot + row selection like h5web in vscode does - x = np.arange(flat.size, dtype=np.float32) + is_2d = data.ndim == 2 + + if is_2d: + n_rows, n_cols = data.shape + row_data = data[0].astype(np.float32) + else: + row_data = data.reshape(-1).astype(np.float32) + + x = np.arange(row_data.size, dtype=np.float32) pw = _styled_plot_widget() pw.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) plot_item = pw.getPlotItem() - - # Prevent intermediate auto-range recalculations during setup plot_item.setAutoVisible(y=False) curve = pg.PlotDataItem( - x, - flat, - pen=pg.mkPen(color=PG_ACCENT, width=1.6), - antialias=False, # critical for 10M points + x, row_data, pen=pg.mkPen(color=PG_ACCENT, width=1.6), antialias=False ) - pw.addItem(curve) - # ---- performance optimizations ---- curve.setDownsampling(auto=True, method="peak") curve.setClipToView(True) - - # optional but often helpful for very large datasets curve.setSkipFiniteCheck(True) - # ---- labels ---- - plot_item.setLabel("bottom", "index") - plot_item.setLabel("left", "value") - - # enable autorange only after item is fully added + # plot_item.setLabel("bottom", "index") + # plot_item.setLabel("left", "value") plot_item.enableAutoRange() - self.stack_layout.addWidget(pw) + if is_2d: + # --- Slider --- + slider = QSlider(Qt.Vertical) + slider.setMinimum(0) + slider.setMaximum(n_rows - 1) + slider.setValue(0) + # slider.setInvertedAppearance(True) # row 0 at top + slider.setFixedWidth(32) + # slider.setSingleStep(1) + slider.setPageStep(1) + + row_label = QLabel("0") + row_label.setAlignment(Qt.AlignCenter) + row_label.setFixedWidth(32) + # row_label.setStyleSheet("color: #aaa; font-size: 10px;") + + def on_row_changed(row): + row_label.setText(str(row)) + new_data = data[row].astype(np.float32) + new_x = np.arange(new_data.size, dtype=np.float32) + curve.setData(new_x, new_data) + plot_item.enableAutoRange() + + slider.valueChanged.connect(on_row_changed) + + slider_col = QWidget() + slider_col.setFixedWidth(36) + col_layout = QVBoxLayout(slider_col) + col_layout.setContentsMargins(0, 0, 0, 0) + col_layout.setSpacing(2) + col_layout.addWidget(row_label) + col_layout.addWidget(slider) + + container = QWidget() + h_layout = QHBoxLayout(container) + h_layout.setContentsMargins(0, 0, 0, 0) + h_layout.setSpacing(4) + h_layout.addWidget(slider_col) + h_layout.addWidget(pw) + + self.stack_layout.addWidget(container) + else: + self.stack_layout.addWidget(pw) # ── 2-D image ────────────────────────────────────────────────────────── def _show_image_2d(self, data):