From a1ce2a43ece9823a55f8df08ad72795e9a21ec89 Mon Sep 17 00:00:00 2001 From: x01da Date: Fri, 12 Jun 2026 22:04:02 +0200 Subject: [PATCH] wip --- .../widgets/data_viewer/data_viewer.py | 67 ++- .../bec_widgets/widgets/data_viewer/viewer.py | 384 +++++++----------- 2 files changed, 165 insertions(+), 286 deletions(-) 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 2a3c5a2..9ed8458 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/data_viewer.py @@ -3,26 +3,20 @@ Data Viewer: Custom BEC widget to view data from scans. """ import sys +from functools import partial + from bec_lib import bec_logger from bec_lib.endpoints import MessageEndpoints from bec_widgets.utils.bec_dispatcher import BECDispatcher from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.colors import apply_theme, get_accent_colors from bec_widgets.utils.error_popups import SafeSlot - -# pylint: disable=E0611 -from qtpy.QtWidgets import ( - QApplication, - QWidget, -) - -from functools import partial - -from bec_widgets.utils.colors import get_accent_colors from qtpy.QtCore import Qt # pylint: disable=E0611 from qtpy.QtGui import QFont + +# pylint: disable=E0611 from qtpy.QtWidgets import ( QApplication, QComboBox, @@ -30,10 +24,10 @@ from qtpy.QtWidgets import ( QGroupBox, QHBoxLayout, QLabel, + QLayout, QPushButton, QVBoxLayout, QWidget, - QLayout, ) from debye_bec.bec_widgets.widgets.data_viewer.qt_widgets import TaggedListWidget @@ -52,7 +46,7 @@ class DataViewer(BECWidget, QWidget): def __init__(self, *arg, parent=None, **kwargs): super().__init__(parent=parent, theme_update=True, *arg, **kwargs) - self.get_bec_shortcuts() + self.get_bec_shortcuts() central = QWidget() self.root_layout = QVBoxLayout(central) @@ -60,17 +54,15 @@ class DataViewer(BECWidget, QWidget): self.input = InputPanel() self.viewer = HDF5Viewer() - self.root_layout.addWidget(self.input, alignment=Qt.AlignmentFlag.AlignTop) - self.root_layout.addWidget(self.viewer, alignment=Qt.AlignmentFlag.AlignTop) + self.root_layout.addWidget(self.input, 0) + self.root_layout.addWidget(self.viewer, 1) self.setLayout(self.root_layout) self.setWindowTitle("Data Viewer") # self.resize(1800, 800) self.history = [] - self.bec_dispatcher.connect_slot( - self.on_history_update, MessageEndpoints.scan_history() - ) + self.bec_dispatcher.connect_slot(self.on_history_update, MessageEndpoints.scan_history()) self.on_history_update() self.current_row = 0 @@ -80,12 +72,12 @@ class DataViewer(BECWidget, QWidget): @SafeSlot() def scan_sel_changed(self, *_, **kwargs): - self.current_row = kwargs['value']().row() + self.current_row = kwargs["value"]().row() @SafeSlot() def load_dataset(self, *_): scan = self.history[self.current_row] - file = scan['file_components'][0] + b'_master.' + scan['file_components'][1] + file = scan["file_components"][0] + b"_master." + scan["file_components"][1] logger.info(file.decode()) self.viewer.load_file(file.decode()) @@ -93,20 +85,22 @@ class DataViewer(BECWidget, QWidget): def on_history_update(self, *_): self.history = [] self.input.scan_sel.clear() - for n in range(1, 10): # last 10 scans - scan_data = self.client.history[-n].metadata['bec'] + # 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 + scan_data = self.client.history[-n].metadata["bec"] # logger.info(scan_data) - self.history.append({ - 'scan_number': scan_data['scan_number'], - 'scan_name': scan_data['scan_name'], - 'user_metadata': scan_data['user_metadata'], - 'file_components': scan_data['file_components'], - }) + self.history.append( + { + "scan_number": scan_data["scan_number"], + "scan_name": scan_data["scan_name"], + "user_metadata": scan_data["user_metadata"], + "file_components": scan_data["file_components"], + } + ) self.input.scan_sel.addTaggedItem( - label=str(scan_data['scan_number']), - tags=[ - (scan_data['scan_name'], "#4A90D9"), - ], + label=str(scan_data["scan_number"]), tags=[(scan_data["scan_name"], "#4A90D9")] ) @@ -120,22 +114,15 @@ 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.load_button = Button(label_button="Load Dataset", 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._layout.addWidget(self.input_group) self._layout.addStretch() - class Group(QGroupBox): def __init__(self, label, widgets): super().__init__(label) diff --git a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py index 0260616..52f73e3 100644 --- a/debye_bec/bec_widgets/widgets/data_viewer/viewer.py +++ b/debye_bec/bec_widgets/widgets/data_viewer/viewer.py @@ -1,126 +1,54 @@ -#!/usr/bin/env python3 """ -HDF5 Viewer — PyQt5 + pyqtgraph + h5py -Usage: python hdf5_viewer.py +HDF5 Viewer — qtpy + pyqtgraph + h5py """ -import sys -import os -import numpy as np import h5py - -from qtpy.QtWidgets import ( - QApplication, QMainWindow, QSplitter, QTreeWidget, QTreeWidgetItem, - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QTableWidget, - QTableWidgetItem, QAbstractItemView, QSizePolicy, QStatusBar, - QRadioButton, QGroupBox, QHeaderView, -) -from qtpy.QtCore import Qt -from qtpy.QtGui import QFont, QColor -from qtpy.QtWidgets import QStyle - +import numpy as np import pyqtgraph as pg +from bec_lib import bec_logger +from bec_qthemes import material_icon +from qtpy.QtCore import Qt +from qtpy.QtGui import QColor, QFont +from qtpy.QtWidgets import ( + QAbstractItemView, + QGroupBox, + QHBoxLayout, + QHeaderView, + QLabel, + QMainWindow, + QRadioButton, + QSizePolicy, + QSplitter, + QTableWidget, + QTableWidgetItem, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, +) + +logger = bec_logger.logger # ── Palette ──────────────────────────────────────────────────────────────── -BG_DARK = "#1C1E26" -BG_MID = "#252731" -BG_PANEL = "#2D3040" -ACCENT = "#7C9CF5" -TEXT_PRI = "#E8EAF2" +ACCENT = "#7C9CF5" TEXT_SEC = "#8A8FA8" -BORDER = "#3A3D50" -SEL_BG = "#3A4268" -GROUP_BG = "#21232D" -SUCCESS = "#5FCEA8" +SUCCESS = "#5FCEA8" # pyqtgraph color tuples (R, G, B) -PG_BG = (28, 30, 38) -PG_PLOT_BG = (45, 48, 64) -PG_ACCENT = (124, 156, 245) -PG_GRID = (58, 61, 80) -PG_TEXT = (138, 143, 168) - -STYLESHEET = f""" -QMainWindow, QWidget {{ - background-color: {BG_DARK}; - color: {TEXT_PRI}; - # font-family: 'Inter', 'Segoe UI', 'Helvetica Neue', sans-serif; - # font-size: 13px; -}} -QSplitter::handle {{ - background: {BORDER}; - width: 1px; height: 1px; -}} -QTreeWidget {{ - background-color: {BG_MID}; - border: 1px solid {BORDER}; - border-radius: 6px; - outline: none; - padding: 4px; -}} -QTreeWidget::item {{ padding: 4px 6px; border-radius: 4px; }} -QTreeWidget::item:hover {{ background: {BG_PANEL}; }} -QTreeWidget::item:selected {{ background: {SEL_BG}; color: {TEXT_PRI}; }} -QHeaderView::section {{ - background-color: {BG_PANEL}; color: {TEXT_SEC}; - border: none; padding: 4px 6px; - font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; -}} -QTableWidget {{ - background-color: {BG_MID}; - border: 1px solid {BORDER}; - border-radius: 6px; - gridline-color: {BORDER}; - outline: none; -}} -QTableWidget::item {{ padding: 3px 8px; color: {TEXT_PRI}; }} -QTableWidget::item:selected {{ background: {SEL_BG}; }} -QTableCornerButton::section {{ background: {BG_PANEL}; border: none; }} -QLabel#section_title {{ - font-size: 11px; font-weight: 600; color: {TEXT_SEC}; - text-transform: uppercase; letter-spacing: 0.08em; padding: 2px 0 6px 2px; -}} -QLabel#info_label {{ color: {TEXT_SEC}; font-size: 12px; padding: 4px; }} -QLabel#path_label {{ color: {ACCENT}; font-size: 12px; font-weight: 500; padding: 2px 4px; }} -QPushButton {{ - background-color: {BG_PANEL}; color: {TEXT_PRI}; - border: 1px solid {BORDER}; border-radius: 5px; padding: 5px 14px; font-size: 12px; -}} -QPushButton:hover {{ background-color: {SEL_BG}; border-color: {ACCENT}; }} -QPushButton:pressed {{ background-color: {ACCENT}; color: white; }} -QRadioButton {{ color: {TEXT_PRI}; spacing: 6px; font-size: 12px; }} -QRadioButton::indicator {{ - width: 14px; height: 14px; border-radius: 7px; - border: 2px solid {BORDER}; background: {BG_MID}; -}} -QRadioButton::indicator:checked {{ background: {ACCENT}; border-color: {ACCENT}; }} -QGroupBox {{ - background: {GROUP_BG}; border: 1px solid {BORDER}; border-radius: 6px; - margin-top: 10px; padding: 8px; font-size: 11px; color: {TEXT_SEC}; -}} -QGroupBox::title {{ - subcontrol-origin: margin; left: 8px; padding: 0 4px; - color: {TEXT_SEC}; text-transform: uppercase; letter-spacing: 0.06em; font-size: 10px; -}} -QStatusBar {{ - background: {BG_PANEL}; color: {TEXT_SEC}; - border-top: 1px solid {BORDER}; font-size: 11px; -}} -QScrollBar:vertical {{ background: {BG_MID}; width: 8px; border-radius: 4px; }} -QScrollBar::handle:vertical {{ background: {BORDER}; border-radius: 4px; min-height: 20px; }} -QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }} -QScrollBar:horizontal {{ background: {BG_MID}; height: 8px; border-radius: 4px; }} -QScrollBar::handle:horizontal {{ background: {BORDER}; border-radius: 4px; min-width: 20px; }} -QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ width: 0; }} -""" +PG_BG = (28, 30, 38) +PG_PLOT_BG = (45, 48, 64) +PG_ACCENT = (124, 156, 245) +PG_GRID = (58, 61, 80) +PG_TEXT = (138, 143, 168) +ICON_SIZE = 20 # ── pyqtgraph global config ──────────────────────────────────────────────── pg.setConfigOptions( background=pg.mkColor(*PG_BG), foreground=pg.mkColor(*PG_TEXT), antialias=True, - useOpenGL=False, # safer default; set True for large datasets if GPU available + useOpenGL=False, # safer default; set True for large datasets if GPU available ) @@ -139,19 +67,26 @@ def _styled_plot_widget(**kwargs) -> pg.PlotWidget: # ── HDF5 Tree helpers ────────────────────────────────────────────────────── TYPE_ICONS = {"group": "📂", "dataset": "📊"} + def dtype_tag(ds): d = ds.dtype - if np.issubdtype(d, np.integer): return "int" - if np.issubdtype(d, np.floating): return "float" - if np.issubdtype(d, np.complexfloating): return "complex" - if d.kind in ("S", "U", "O"): return "str" + if np.issubdtype(d, np.integer): + return "int" + if np.issubdtype(d, np.floating): + return "float" + if np.issubdtype(d, np.complexfloating): + return "complex" + if d.kind in ("S", "U", "O"): + return "str" return str(d) def populate_tree(parent_item, h5_obj): - style = QApplication.style() - folder_icon = style.standardIcon(QStyle.SP_DirIcon) - file_icon = style.standardIcon(QStyle.SP_FileIcon) + folder_icon = material_icon("folder", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + vector_icon = material_icon("show_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + array_icon = material_icon("stacked_line_chart", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + scalar_icon = material_icon("point_scan", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + str_icon = material_icon("text_snippet", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") if not isinstance(h5_obj, h5py.Group): return @@ -174,10 +109,22 @@ def populate_tree(parent_item, h5_obj): populate_tree(item, child) elif isinstance(child, h5py.Dataset): - shape_str = "×".join(str(s) for s in child.shape) or "scalar" + shape_str = "x".join(str(s) for s in child.shape) or "scalar" - item = QTreeWidgetItem(parent_item, [key, dtype_tag(child), shape_str]) - item.setIcon(0, file_icon) + dtype = dtype_tag(child) + + if shape_str == "scalar": + if dtype == "str": + icon = str_icon + else: + icon = scalar_icon + elif "x" in shape_str: + icon = array_icon + else: + icon = vector_icon + + item = QTreeWidgetItem(parent_item, [key, dtype, shape_str]) + item.setIcon(0, icon) item.setData(0, Qt.UserRole, child.name) item.setData(0, Qt.UserRole + 1, "dataset") @@ -191,8 +138,6 @@ class DataPanel(QWidget): def __init__(self): super().__init__() self._layout = QVBoxLayout(self) - self._layout.setContentsMargins(0, 0, 0, 0) - self._layout.setSpacing(8) # Header hdr = QHBoxLayout() @@ -208,8 +153,8 @@ class DataPanel(QWidget): mode_box = QGroupBox("View mode") mode_layout = QHBoxLayout(mode_box) mode_layout.setContentsMargins(8, 4, 8, 4) - self.rb_auto = QRadioButton("Auto") - self.rb_plot = QRadioButton("Plot") + self.rb_auto = QRadioButton("Auto") + self.rb_plot = QRadioButton("Plot") self.rb_image = QRadioButton("Image") self.rb_table = QRadioButton("Table") self.rb_auto.setChecked(True) @@ -247,9 +192,12 @@ class DataPanel(QWidget): self.display(self._current_data, self.path_label.text()) def _active_mode(self): - if self.rb_plot.isChecked(): return "plot" - if self.rb_image.isChecked(): return "image" - if self.rb_table.isChecked(): return "table" + if self.rb_plot.isChecked(): + return "plot" + if self.rb_image.isChecked(): + return "image" + if self.rb_table.isChecked(): + return "table" return "auto" # ── public ──────────────────────────────────────────────────────────── @@ -273,7 +221,9 @@ class DataPanel(QWidget): else: mode = "table" - if mode == "plot": + if ( + mode == "plot" + ): # TODO disable plot and image if data is str, maybe completely disable manual mode and only allow image/plot for arrays self._show_plot_1d(data) elif mode == "image": self._show_image_2d(data) @@ -283,25 +233,42 @@ class DataPanel(QWidget): # ── 1-D line plot ────────────────────────────────────────────────────── def _show_plot_1d(self, data): self._clear_stack() - flat = data.flatten().astype(float) + + 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) pw = _styled_plot_widget() pw.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - # Filled area under curve - x = np.arange(len(flat), dtype=float) - fill = pg.FillBetweenItem( - pg.PlotDataItem(x, flat), - pg.PlotDataItem(x, np.zeros_like(flat)), - brush=pg.mkBrush(124, 156, 245, 35), - ) - pw.addItem(fill) - pw.plot(x, flat, - pen=pg.mkPen(color=PG_ACCENT, width=1.6), - antialias=True) + plot_item = pw.getPlotItem() - pw.getPlotItem().setLabel("bottom", "index") - pw.getPlotItem().setLabel("left", "value") + # 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 + ) + + 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.enableAutoRange() self.stack_layout.addWidget(pw) @@ -318,7 +285,7 @@ class DataPanel(QWidget): # ImageView gives us colorbar + histogram + zoom for free iv = pg.ImageView() - iv.setBackground(pg.mkColor(*PG_BG)) + iv.getView().setBackgroundColor(pg.mkColor(*PG_BG)) iv.ui.histogram.setBackground(pg.mkColor(*PG_PLOT_BG)) iv.ui.roiBtn.hide() iv.ui.menuBtn.hide() @@ -365,13 +332,18 @@ class DataPanel(QWidget): table.verticalHeader().setDefaultSectionSize(22) table.setFont(QFont("JetBrains Mono, Consolas, monospace", 10)) - is_float = np.issubdtype(flat.dtype, np.floating) + is_float = np.issubdtype(flat.dtype, np.floating) is_complex = np.iscomplexobj(flat) + is_bytes = flat.dtype.kind == "S" for r in range(show_rows): for c in range(show_cols): val = flat[r, c] - txt = f"{val:.6g}" if is_float else (f"{val:.4g}" if is_complex else str(val)) + txt = ( + f"{val:.6g}" + if is_float + else f"{val:.4g}" if is_complex else str(val.decode()) if is_bytes else str(val) + ) cell = QTableWidgetItem(txt) cell.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) table.setItem(r, c, cell) @@ -379,75 +351,32 @@ class DataPanel(QWidget): self.stack_layout.addWidget(table) -# ── Attributes panel ─────────────────────────────────────────────────────── -class AttrsPanel(QWidget): - def __init__(self): - super().__init__() - layout = QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - title = QLabel("Attributes") - title.setObjectName("section_title") - layout.addWidget(title) - self.table = QTableWidget(0, 2) - self.table.setHorizontalHeaderLabels(["Key", "Value"]) - self.table.horizontalHeader().setStretchLastSection(True) - self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.table.verticalHeader().hide() - self.table.verticalHeader().setDefaultSectionSize(22) - layout.addWidget(self.table) - - def set_attrs(self, attrs_dict): - self.table.setRowCount(0) - for key, val in attrs_dict.items(): - row = self.table.rowCount() - self.table.insertRow(row) - self.table.setItem(row, 0, QTableWidgetItem(str(key))) - self.table.setItem(row, 1, QTableWidgetItem(str(val))) - - # ── Main window ──────────────────────────────────────────────────────────── class HDF5Viewer(QMainWindow): def __init__(self, filepath=None): super().__init__() self.filepath = filepath - self.h5file = None - # self.setWindowTitle(f"HDF5 Viewer — {os.path.basename(filepath)}") - # self.resize(1280, 800) + self.h5file = None self._build_ui() def load_file(self, f): self.filepath = f - self.h5file = h5py.File(f, "r") - self._load_tree() + self.h5file = h5py.File(f, "r") + filename = f.split("/")[-1] + self._load_tree(filename) def _build_ui(self): central = QWidget() self.setCentralWidget(central) - root_layout = QVBoxLayout(central) - root_layout.setContentsMargins(10, 10, 10, 6) - root_layout.setSpacing(6) - # Top bar - top_bar = QHBoxLayout() - # file_label = QLabel(f"📁 {self.filepath}") - # file_label.setObjectName("path_label") - # top_bar.addWidget(file_label) - top_bar.addStretch() - root_layout.addLayout(top_bar) + root_layout = QHBoxLayout(central) - # Main horizontal splitter - h_split = QSplitter(Qt.Horizontal) - h_split.setHandleWidth(2) + splitter = QSplitter(Qt.Horizontal) + splitter.setChildrenCollapsible(False) - # ── Left: tree + attrs ── - left_pane = QWidget() + # ── Left pane ── + left_pane = QWidget() left_layout = QVBoxLayout(left_pane) - left_layout.setContentsMargins(0, 0, 0, 0) - left_layout.setSpacing(6) - - tree_title = QLabel("Structure") - tree_title.setObjectName("section_title") - left_layout.addWidget(tree_title) self.tree = QTreeWidget() self.tree.setHeaderLabels(["Name", "Type", "Shape"]) @@ -455,36 +384,31 @@ class HDF5Viewer(QMainWindow): self.tree.header().setSectionResizeMode(0, QHeaderView.Stretch) self.tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents) self.tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents) - self.tree.setIndentation(16) self.tree.itemClicked.connect(self._on_item_clicked) - left_layout.addWidget(self.tree, 3) - self.attrs_panel = AttrsPanel() - left_layout.addWidget(self.attrs_panel, 1) + left_layout.addWidget(self.tree, 1) - h_split.addWidget(left_pane) - - # ── Right: data panel ── + # ── Right pane ── self.data_panel = DataPanel() - h_split.addWidget(self.data_panel) - h_split.setStretchFactor(0, 1) - h_split.setStretchFactor(1, 3) - h_split.setSizes([320, 960]) + splitter.addWidget(left_pane) + splitter.addWidget(self.data_panel) - root_layout.addWidget(h_split, 1) + # Initial proportions: 25% / 75% + splitter.setStretchFactor(0, 1) + splitter.setStretchFactor(1, 3) + splitter.setSizes([300, 900]) + splitter.setHandleWidth(6) + splitter.setChildrenCollapsible(False) - # self.status = QStatusBar() - # self.setStatusBar(self.status) - # self.status.showMessage(f"Loaded: {self.filepath}") + root_layout.addWidget(splitter) - def _load_tree(self): + def _load_tree(self, filename): self.tree.clear() - style = QApplication.style() - folder_icon = style.standardIcon(QStyle.SP_DirIcon) # TODO Choose different icon for head - root_item = QTreeWidgetItem(self.tree, ["/", "Group", ""]) - root_item.setIcon(0, folder_icon) + dataset_icon = material_icon("dataset", size=(ICON_SIZE, ICON_SIZE), color="#2980b9") + root_item = QTreeWidgetItem(self.tree, [filename, "Group", ""]) + root_item.setIcon(0, dataset_icon) root_item.setData(0, Qt.UserRole, "/") root_item.setData(0, Qt.UserRole + 1, "group") root_item.setForeground(0, QColor(ACCENT)) @@ -506,46 +430,14 @@ class HDF5Viewer(QMainWindow): return obj = self.h5file[path] if path != "/" else self.h5file - attrs = dict(obj.attrs) if obj.attrs else {} - self.attrs_panel.set_attrs(attrs) if kind == "dataset": - ds = self.h5file[path] + ds = self.h5file[path] data = ds[()] self.data_panel.display(data, path) - # self.status.showMessage( - # f"{path} | shape {ds.shape} | dtype {ds.dtype}" - # ) else: n = len(obj) if isinstance(obj, h5py.Group) else 0 - # self.status.showMessage( - # f"{path} | Group | {n} items | {len(attrs)} attributes" - # ) def closeEvent(self, event): self.h5file.close() super().closeEvent(event) - - -# ── Entry point ──────────────────────────────────────────────────────────── -def main(): - if len(sys.argv) < 2: - print("Usage: python hdf5_viewer.py ") - sys.exit(1) - - filepath = sys.argv[1] - if not os.path.isfile(filepath): - print(f"Error: file not found — {filepath}") - sys.exit(1) - - app = QApplication(sys.argv) - app.setApplicationName("HDF5 Viewer") - app.setStyleSheet(STYLESHEET) - - window = HDF5Viewer(filepath) - window.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - main() \ No newline at end of file