@@ -2,9 +2,12 @@
|
||||
Data Viewer: Custom BEC widget to view data from scans.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
@@ -36,6 +39,8 @@ from debye_bec.bec_widgets.widgets.data_viewer.viewer import HDF5Viewer
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MAX_HIST_LEN = 100
|
||||
|
||||
|
||||
class DataViewer(BECWidget, QWidget):
|
||||
"""
|
||||
@@ -43,7 +48,7 @@ class DataViewer(BECWidget, QWidget):
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "lightbulb"
|
||||
ICON_NAME = "database_search" # TODO: This does not work, choose correct icon
|
||||
|
||||
def __init__(self, *arg, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, theme_update=True, *arg, **kwargs)
|
||||
@@ -76,11 +81,33 @@ 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)
|
||||
self.input.open_fm_button.clicked_connect(self.open_in_file_manager)
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
self.viewer.apply_theme(theme)
|
||||
|
||||
@SafeSlot()
|
||||
def scan_sel_changed(self, *_, **kwargs):
|
||||
self.current_row = kwargs["value"]().row()
|
||||
|
||||
@SafeSlot()
|
||||
def open_in_file_manager(self, *_):
|
||||
scan = self.history[self.current_row]
|
||||
filepath = scan["file_components"][0].decode().rsplit("/", 1)[0]
|
||||
subprocess.Popen(
|
||||
["xdg-open", filepath],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
stdin=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def load_dataset(
|
||||
self, *_
|
||||
@@ -121,12 +148,12 @@ class DataViewer(BECWidget, QWidget):
|
||||
self.input.scan_sel.clear()
|
||||
# Get the length of the scan history, which is 0 when the bec server was started
|
||||
# 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
|
||||
max_scans = min(len(self.client.history), MAX_HIST_LEN)
|
||||
for n in range(1, max_scans): # last scans, limited by MAX_HIST_LEN
|
||||
# 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))
|
||||
# logger.info(type(start_time))
|
||||
scan_data = self.client.history[-n].metadata["bec"]
|
||||
# logger.info(scan_data)
|
||||
scan_number = scan_data["scan_number"]
|
||||
@@ -160,7 +187,7 @@ class DataViewer(BECWidget, QWidget):
|
||||
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):
|
||||
@@ -168,29 +195,44 @@ class InputPanel(QWidget):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
self._layout = QHBoxLayout(self)
|
||||
# self._layout.setSizeConstraint(QLayout.SetFixedSize) # type: ignore
|
||||
|
||||
# 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)
|
||||
self.open_fm_button = Button(label_button="Open in File Manager", enabled=True)
|
||||
|
||||
self._button_layout = QVBoxLayout()
|
||||
self._button_layout.addWidget(self.load_button)
|
||||
self._button_layout.addWidget(self.unload_button)
|
||||
self._button_layout.addWidget(self.open_fm_button)
|
||||
self._button_layout.addStretch()
|
||||
|
||||
# Assemble complete scan selection group
|
||||
self.input_group = Group(
|
||||
"Scan selection", [self.scan_sel, self.load_button, self.unload_button]
|
||||
"Scan selection", [self._button_layout, self.scan_sel], orientation="horizontal"
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.input_group)
|
||||
self._layout.addStretch()
|
||||
# self._layout.addStretch()
|
||||
|
||||
|
||||
class Group(QGroupBox):
|
||||
def __init__(self, label, widgets):
|
||||
def __init__(self, label, objs, orientation="vertical"):
|
||||
super().__init__(label)
|
||||
self.layout = QVBoxLayout(self) # type: ignore
|
||||
for widget in widgets:
|
||||
self.layout.addWidget(widget) # type: ignore
|
||||
if orientation == "vertical":
|
||||
self._layout = QVBoxLayout(self) # type: ignore
|
||||
elif orientation == "horizontal": # assume horizontal
|
||||
self._layout = QHBoxLayout(self) # type: ignore
|
||||
else:
|
||||
raise ValueError(f"Orientation {orientation} is not supported!")
|
||||
for obj in objs:
|
||||
if isinstance(obj, QWidget):
|
||||
self._layout.addWidget(obj) # type: ignore
|
||||
elif isinstance(obj, QLayout):
|
||||
self._layout.addLayout(obj)
|
||||
|
||||
|
||||
class ListWidget(QWidget):
|
||||
@@ -206,7 +248,7 @@ class ListWidget(QWidget):
|
||||
# self.label.setWordWrap(True)
|
||||
# layout.addWidget(self.label)
|
||||
self.value = TaggedListWidget()
|
||||
self.value.setFixedWidth(400)
|
||||
# self.value.setFixedWidth(400)
|
||||
# for entry in enums:
|
||||
# self.value.addItem(entry)
|
||||
layout.addWidget(self.value)
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
HDF5 Viewer — qtpy + pyqtgraph + h5py
|
||||
"""
|
||||
|
||||
from typing import Literal, Optional, cast
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from bec_widgets.utils.colors import Colors
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QColor, QFont
|
||||
from qtpy.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
@@ -58,9 +62,9 @@ def _styled_plot_widget(**kwargs) -> pg.PlotWidget:
|
||||
pw = pg.PlotWidget(**kwargs)
|
||||
# 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))
|
||||
ax.getAxis(side).setTextPen(pg.mkPen(color=PG_TEXT))
|
||||
# for side in ("left", "bottom", "right", "top"):
|
||||
# ax.getAxis(side).setPen(pg.mkPen(color=PG_GRID, width=1))
|
||||
# ax.getAxis(side).setTextPen(pg.mkPen(color=PG_TEXT))
|
||||
ax.showGrid(x=True, y=True, alpha=0.25)
|
||||
return pw
|
||||
|
||||
@@ -171,6 +175,9 @@ class DataPanel(QWidget):
|
||||
self.stack_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.addWidget(self.stack, 1)
|
||||
|
||||
self.plot = None
|
||||
self.images = []
|
||||
|
||||
self._current_data = None
|
||||
self._show_empty()
|
||||
|
||||
@@ -246,6 +253,8 @@ class DataPanel(QWidget):
|
||||
x = np.arange(row_data.size, dtype=np.float32)
|
||||
|
||||
pw = _styled_plot_widget()
|
||||
self.plot = pw
|
||||
self.apply_theme()
|
||||
pw.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
plot_item = pw.getPlotItem()
|
||||
@@ -308,10 +317,49 @@ class DataPanel(QWidget):
|
||||
else:
|
||||
self.stack_layout.addWidget(pw)
|
||||
|
||||
def apply_theme(self, theme: Optional[Literal["dark", "light"]] = None):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (Optional[str]): Theme, either "dark", "light", or None. Defaults to None.
|
||||
"""
|
||||
if theme is None:
|
||||
app = QApplication.instance()
|
||||
theme = app.theme.theme # type: ignore
|
||||
|
||||
bg_color = pg.getConfigOption("background")
|
||||
fg_color = pg.getConfigOption("foreground")
|
||||
try: # TODO: Remove try/except (plot or image might already be deleted!)
|
||||
n_curves = len(self.plot.listDataItems())
|
||||
colors = Colors.golden_angle_color(
|
||||
colormap="plasma", num=max(10, n_curves + 1), format="HEX"
|
||||
)
|
||||
for idx, curve in enumerate(self.plot.listDataItems()):
|
||||
curve.setPen(pg.mkPen(color=colors[idx]))
|
||||
# Background
|
||||
self.plot.setBackground(bg_color)
|
||||
# Axes (tick marks, tick labels, axis line)
|
||||
for axis in ["left", "bottom", "right", "top"]:
|
||||
ax = self.plot.getAxis(axis)
|
||||
ax.setPen(pg.mkPen(color=fg_color))
|
||||
ax.setTextPen(pg.mkPen(color=fg_color))
|
||||
except:
|
||||
pass
|
||||
|
||||
for image in self.images:
|
||||
try:
|
||||
image.getView().setBackgroundColor(bg_color)
|
||||
image.ui.histogram.setBackground(bg_color)
|
||||
except:
|
||||
pass
|
||||
|
||||
# ── 2-D image ──────────────────────────────────────────────────────────
|
||||
def _show_image_2d(self, data):
|
||||
self._clear_stack()
|
||||
|
||||
self.images = []
|
||||
|
||||
squeezed = np.squeeze(data)
|
||||
if squeezed.ndim > 2:
|
||||
squeezed = squeezed.reshape(-1, squeezed.shape[-1])
|
||||
@@ -321,8 +369,10 @@ class DataPanel(QWidget):
|
||||
|
||||
# ImageView gives us colorbar + histogram + zoom for free
|
||||
iv = pg.ImageView()
|
||||
iv.getView().setBackgroundColor(pg.mkColor(*PG_BG))
|
||||
iv.ui.histogram.setBackground(pg.mkColor(*PG_PLOT_BG))
|
||||
self.images.append(iv)
|
||||
self.apply_theme()
|
||||
# iv.getView().setBackgroundColor(pg.mkColor(*PG_BG))
|
||||
# iv.ui.histogram.setBackground(pg.mkColor(*PG_PLOT_BG))
|
||||
iv.ui.roiBtn.hide()
|
||||
iv.ui.menuBtn.hide()
|
||||
|
||||
@@ -396,6 +446,15 @@ class HDF5Viewer(QMainWindow):
|
||||
if filepath:
|
||||
self.load_files([filepath])
|
||||
|
||||
def apply_theme(self, theme: Literal["dark", "light"]):
|
||||
"""
|
||||
Apply the theme
|
||||
|
||||
Args:
|
||||
theme (str): Theme, either "dark" or "light"
|
||||
"""
|
||||
self.data_panel.apply_theme(theme)
|
||||
|
||||
def load_files(self, filepaths: list[str]):
|
||||
"""Open one or more HDF5 files and add each as a top-level tree node."""
|
||||
for f in filepaths:
|
||||
|
||||
Reference in New Issue
Block a user