Source code for pygra.widgets

"""
widgets.py — DatasetWidget: compact per-series panel
"""

import numpy as np

from PyQt5.QtWidgets import (
    QWidget, QVBoxLayout, QGridLayout, QPushButton, QLabel,
    QSpinBox, QCheckBox, QScrollArea, QFrame,
    QMessageBox, QFileDialog, QHBoxLayout, QRadioButton, QButtonGroup,
)

from .constants import MARKERS, MARKER_LABELS, LINESTYLES, LINESTYLE_LABELS
from .dialogs import AppearanceDialog, HistAppearanceDialog, Hist2DAppearanceDialog


[docs] class DatasetWidget(QWidget): """ Compact per-series control panel embedded in the left tab widget. Shows dataset name and dimensions, a histogram-mode toggle, column selectors for x, y, dx, dy (line mode) or a single column (histogram mode), an Appearance button, a duplicate-series button, and a visibility checkbox. Style details are edited through :class:`~dialogs.AppearanceDialog` or :class:`~dialogs.HistAppearanceDialog`. Parameters ---------- dataset : DataSet The dataset this widget controls. color : str Initial hex color string for the series (e.g. ``"#1f77b4"``). on_duplicate : callable, optional Called with *dataset* when the "Add another plot" button is clicked. on_replot : callable, optional Called with no arguments whenever a style change requires a full replot. parent : QWidget, optional Parent widget. """ def __init__(self, dataset, color: str, on_duplicate=None, on_replot=None, parent=None): super().__init__(parent) self.dataset = dataset self.on_duplicate = on_duplicate self.on_replot = on_replot # callable: triggers a full replot # series style self._series_style = { "label": dataset.name, "linestyle": "-", "linewidth": 1.8, "marker": "o", "markersize": 5.0, "color": color, "face_color": color, } # histogram style self._hist_style = { "hist_bins": "auto", "hist_nbins": 20, "hist_norm": "count", "hist_horizontal": False, "hist_show_pct": False, "pct_fontsize": 9.0, "hist_color_by_value": False, "hist_colormap": "viridis", "color": color, "face_color": color, } # 2D histogram style self._hist2d_style = { "bins_x": 0, # 0 = auto "bins_y": 0, # 0 = auto "colormap": "viridis", "norm": "count", "log_scale": False, "colorbar": True, } self._build() # ------------------------------------------------------------------ # UI # ------------------------------------------------------------------ def _build(self): scroll = QScrollArea(self) scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.NoFrame) inner = QWidget() layout = QGridLayout(inner) layout.setContentsMargins(4, 4, 4, 4) layout.setSpacing(5) nc = self.dataset.ncols nr = self.dataset.nrows layout.addWidget(QLabel(f"<b>{self.dataset.name}</b>"), 0, 0, 1, 4) layout.addWidget(QLabel(f"{nr} rows, {nc} cols"), 1, 0, 1, 4) # mode radio buttons self._mode_group = QButtonGroup(self) self._rb_series = QRadioButton("Series") self._rb_hist = QRadioButton("Histogram") self._rb_hist2d = QRadioButton("Histogram 2D") self._rb_series.setChecked(True) self._mode_group.addButton(self._rb_series, 0) self._mode_group.addButton(self._rb_hist, 1) self._mode_group.addButton(self._rb_hist2d, 2) rb_widget = QWidget() rb_layout = QHBoxLayout(rb_widget) rb_layout.setContentsMargins(0, 0, 0, 0) rb_layout.addWidget(self._rb_series) rb_layout.addWidget(self._rb_hist) rb_layout.addWidget(self._rb_hist2d) layout.addWidget(rb_widget, 2, 0, 1, 4) self._mode_group.buttonClicked.connect(lambda _: self._toggle_mode()) def col_spin(default): s = QSpinBox() s.setMinimum(-1) s.setMaximum(max(0, nc - 1)) s.setValue(default if default < nc else -1) s.setSpecialValueText("—") s.setFixedWidth(55) return s # series columns (x, y, dx, dy) self._lbl_x = QLabel("x:"); self.xcol = col_spin(0) self._lbl_y = QLabel("y:"); self.ycol = col_spin(1) self._lbl_dx = QLabel("dx:"); self.dxcol = col_spin(-1) self._lbl_dy = QLabel("dy:"); self.dycol = col_spin(-1) layout.addWidget(self._lbl_x, 3, 0); layout.addWidget(self.xcol, 3, 1) layout.addWidget(self._lbl_y, 3, 2); layout.addWidget(self.ycol, 3, 3) layout.addWidget(self._lbl_dx, 4, 0); layout.addWidget(self.dxcol, 4, 1) layout.addWidget(self._lbl_dy, 4, 2); layout.addWidget(self.dycol, 4, 3) # histogram column self._lbl_hcol = QLabel("column:") self.hcol = col_spin(0) layout.addWidget(self._lbl_hcol, 3, 0); layout.addWidget(self.hcol, 3, 1) # hist2d columns self._lbl_xcol2 = QLabel("x:"); self.xcol2 = col_spin(0) self._lbl_ycol2 = QLabel("y:"); self.ycol2 = col_spin(1) layout.addWidget(self._lbl_xcol2, 3, 0); layout.addWidget(self.xcol2, 3, 1) layout.addWidget(self._lbl_ycol2, 3, 2); layout.addWidget(self.ycol2, 3, 3) # appearance button self.appear_btn = QPushButton("Appearance...") self.appear_btn.clicked.connect(self._open_appearance) layout.addWidget(self.appear_btn, 6, 0, 1, 4) # duplicate dup_btn = QPushButton("Add another plot from this file") dup_btn.clicked.connect(self._duplicate) layout.addWidget(dup_btn, 7, 0, 1, 4) # visibility self.visible = QCheckBox("visible"); self.visible.setChecked(True) layout.addWidget(self.visible, 8, 0, 1, 4) scroll.setWidget(inner) outer = QVBoxLayout(self) outer.setContentsMargins(0, 0, 0, 0) outer.addWidget(scroll) self._toggle_mode() # ------------------------------------------------------------------ # Mode switching # ------------------------------------------------------------------ def _toggle_mode(self): series = self._rb_series.isChecked() hist = self._rb_hist.isChecked() hist2d = self._rb_hist2d.isChecked() for w in [self._lbl_x, self.xcol, self._lbl_y, self.ycol, self._lbl_dx, self.dxcol, self._lbl_dy, self.dycol]: w.setVisible(series) for w in [self._lbl_hcol, self.hcol]: w.setVisible(hist) for w in [self._lbl_xcol2, self.xcol2, self._lbl_ycol2, self.ycol2]: w.setVisible(hist2d)
[docs] def set_mode(self, mode: str): """ Set display mode programmatically. Parameters ---------- mode : str One of ``"series"``, ``"histogram"``, or ``"hist2d"``. """ if mode == "histogram": self._rb_hist.setChecked(True) elif mode == "hist2d": self._rb_hist2d.setChecked(True) else: self._rb_series.setChecked(True) self._toggle_mode()
# ------------------------------------------------------------------ # Appearance # ------------------------------------------------------------------ def _open_appearance(self): if self._rb_hist.isChecked(): cfg = dict(self._hist_style) dlg = HistAppearanceDialog(cfg, self) if dlg.exec_(): self._hist_style.update(dlg.get_config()) if self.on_replot: self.on_replot() elif self._rb_hist2d.isChecked(): cfg = dict(self._hist2d_style) dlg = Hist2DAppearanceDialog(cfg, self) if dlg.exec_(): self._hist2d_style.update(dlg.get_config()) if self.on_replot: self.on_replot() else: cfg = dict(self._series_style) dlg = AppearanceDialog(cfg, self) if dlg.exec_(): self._series_style.update(dlg.get_config()) if self.on_replot: self.on_replot() # ------------------------------------------------------------------ # Actions # ------------------------------------------------------------------ def _duplicate(self): if self.on_duplicate: self.on_duplicate(self.dataset)
[docs] def refresh_col_ranges(self): """ Update the maximum value of all column spinboxes. Should be called after the underlying dataset gains or loses columns (e.g. after a transform operation adds a new column). """ nc = max(0, self.dataset.ncols - 1) for w in [self.xcol, self.ycol, self.dxcol, self.dycol, self.hcol, self.xcol2, self.ycol2]: w.setMaximum(nc)
# ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------
[docs] def get_config(self) -> dict: """ Return the current widget configuration as a plotting dict. In series mode the result includes series-style keys; in histogram mode it includes histogram-style keys; in hist2d mode it includes hist2d-style keys. Returns ------- dict Keys always present: ``"label"`` (str), ``"visible"`` (bool), ``"hist_mode"`` (bool), ``"hist2d_mode"`` (bool), ``"xcol"``, ``"ycol"``, ``"dxcol"``, ``"dycol"``, ``"hcol"`` (int). In hist2d mode ``"xcol"`` and ``"ycol"`` reflect the hist2d column spinboxes. Additional style keys are merged from the active style dict. """ hist = self._rb_hist.isChecked() hist2d = self._rb_hist2d.isChecked() cfg = { "label": self._series_style.get("label", self.dataset.name), "visible": self.visible.isChecked(), "hist_mode": hist, "hist2d_mode": hist2d, "xcol": self.xcol2.value() if hist2d else self.xcol.value(), "ycol": self.ycol2.value() if hist2d else self.ycol.value(), "dxcol": self.dxcol.value(), "dycol": self.dycol.value(), "hcol": self.hcol.value(), } if hist: cfg.update(self._hist_style) elif hist2d: cfg.update(self._hist2d_style) else: cfg.update(self._series_style) return cfg