wip
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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: <b>{label}</b> tags: {tag_str}")
|
||||
|
||||
lst.currentItemChanged.connect(lambda *_: on_selection())
|
||||
layout.addWidget(info)
|
||||
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user