fix graph and white background added

This commit is contained in:
Ayzen
2026-10-15 14:46:06 +03:00
parent 5102ed751c
commit 9a9b8ce5c7
35 changed files with 730 additions and 463 deletions
+32 -46
View File
@@ -6,10 +6,10 @@ from typing import Protocol
import numpy as np
import pyqtgraph as pg
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPen
from PyQt6.QtWidgets import QVBoxLayout, QWidget
from python_app.gui.controllers.renderers.common import create_plot
from python_app.gui.controllers.renderers.common import CAPTURED_TRACE_PEN, OVERLAY_FILL, SERIES_COLORS, create_plot
from python_app.models.dataset_model import ComboKey, ResultCollection, TraceData
from python_app.models.gui_profile_schema import GuiPassThroughStateModel
from python_app.models.run_config_validation import parse_combos_from_text
@@ -21,9 +21,7 @@ class WarningLogger(Protocol):
CurveKey = tuple[ComboKey, int, str]
Series = tuple[CurveKey, np.ndarray, np.ndarray, str]
_PALETTE = ("#4cc9f0", "#f72585", "#b8f2e6", "#ffd166",
"#90be6d", "#ff595e", "#6a4c93", "#1982c4")
Series = tuple[CurveKey, np.ndarray, np.ndarray, QPen]
class _TracePanel:
@@ -35,57 +33,49 @@ class _TracePanel:
assert item is not None
self.item = item
self.view = item.getViewBox()
self.item.showGrid(x=True, y=True, alpha=0.2)
self.item.setLabel("left", label, units=units)
self.item.setLabel("bottom", "Frequency", units="Hz")
self.item.setDownsampling(mode="peak")
self.item.setClipToView(True)
self.curves: dict[CurveKey, pg.PlotCurveItem] = {}
self.legend: pg.LegendItem | None = None
self._legend_sources: dict[ComboKey, pg.PlotCurveItem] = {}
# Sweeps denser than the screen are drawn as per-bin min/max, so narrow peaks
# and notches stay visible; ordinary sweeps keep every point.
self.item.setDownsampling(auto=True, mode="peak")
self.curves: dict[CurveKey, pg.PlotDataItem] = {}
self.legend = self.item.addLegend(offset=(8, 8), brush=pg.mkBrush(*OVERLAY_FILL), colCount=2)
self.legend.hide()
self._legend_sources: dict[ComboKey, pg.PlotDataItem] = {}
def clear(self) -> None:
self.item.clear()
self.curves.clear()
self.sync_legend({})
def update_curve(self, key: CurveKey, x: np.ndarray, y: np.ndarray,
color: str, *, width: float, dashed: bool = False) -> pg.PlotCurveItem:
pen = pg.mkPen(color, width=width,
style=Qt.PenStyle.DashLine if dashed else Qt.PenStyle.SolidLine)
def update_curve(self, key: CurveKey, x: np.ndarray, y: np.ndarray, pen: QPen) -> pg.PlotDataItem:
curve = self.curves.get(key)
if curve is None:
curve = pg.PlotCurveItem(pen=pen, antialias=False)
curve = pg.PlotDataItem(antialias=False)
self.curves[key] = curve
self.item.addItem(curve)
else:
curve.setPen(pen)
curve.setData(x, y)
curve.setData(x, y, pen=pen) # one update; a separate setPen would redraw the old data first
return curve
def retain_curves(self, active_keys: set[CurveKey]) -> None:
for key in self.curves.keys() - active_keys:
self.item.removeItem(self.curves.pop(key))
def sync_legend(self, sources: dict[ComboKey, pg.PlotCurveItem]) -> None:
def sync_legend(self, sources: dict[ComboKey, pg.PlotDataItem]) -> None:
if self._legend_sources == sources:
return
if self.legend is None and sources:
self.legend = self.item.addLegend(offset=(8, 8))
if self.legend is not None:
self.legend.clear()
for combo in sorted(sources, key=lambda key: (key.input, key.output)):
self.legend.addItem(sources[combo], f"in{combo.input}/out{combo.output}")
self.legend.setVisible(bool(sources))
self.legend.clear()
for combo in sorted(sources, key=lambda key: (key.input, key.output)):
self.legend.addItem(sources[combo], f"in{combo.input}/out{combo.output}")
self.legend.setVisible(bool(sources))
self._legend_sources = sources
class TraceRenderer:
"""Render result traces or a captured trace using explicit display settings."""
def __init__(self, log_warning: WarningLogger, max_points: int = 800) -> None:
def __init__(self, log_warning: WarningLogger) -> None:
self._log_warning = log_warning
self.max_points = max_points
self.widget = QWidget()
layout = QVBoxLayout(self.widget)
layout.setContentsMargins(0, 0, 0, 0)
@@ -102,23 +92,24 @@ class TraceRenderer:
def draw(self, collection: ResultCollection, settings: GuiPassThroughStateModel) -> bool:
combo_filter = self._combo_filter(settings.combo_filter)
colors: dict[ComboKey, str] = {}
series: list[Series] = []
for block in collection.blocks:
# The color follows the pair's place in the run, so filtering never repaints shown pairs.
for position, block in enumerate(collection.blocks):
if combo_filter is not None and block.combo not in combo_filter:
continue
color = colors.setdefault(block.combo, _PALETTE[len(colors) % len(_PALETTE)])
pen = pg.mkPen(SERIES_COLORS[position % len(SERIES_COLORS)], width=2)
for index, payload in enumerate(block.payloads):
if payload.kind != 1 or not payload.trace.size or payload.frequency_hz.size != payload.trace.size:
# Another mode's result (a B-scan range profile) is not S21 over frequency.
if payload.processing_name != "pass_through" or payload.kind != 1:
continue
series.append(((block.combo, index, payload.processing_name),
payload.frequency_hz, payload.trace, color))
payload.frequency_hz, payload.trace, pen))
return self._render(series, settings, "Pass-Through S21")
def draw_trace(self, trace: TraceData, title: str, settings: GuiPassThroughStateModel) -> None:
self.clear()
self._render([((trace.combo, 0, "__single_trace__"), trace.frequency_hz,
trace.s21, "#ffd166")], settings, title, single=True)
trace.s21, CAPTURED_TRACE_PEN)], settings, title, single=True)
def _combo_filter(self, text: str) -> set[ComboKey] | None:
text = text.strip()
@@ -142,27 +133,23 @@ class TraceRenderer:
return False
mag.item.showAxis("bottom", show=not settings.show_phase)
active: set[CurveKey] = set()
mag_sources: dict[ComboKey, pg.PlotCurveItem] = {}
phase_sources: dict[ComboKey, pg.PlotCurveItem] = {}
mag_sources: dict[ComboKey, pg.PlotDataItem] = {}
phase_sources: dict[ComboKey, pg.PlotDataItem] = {}
x_min, x_max = np.inf, -np.inf
for key, frequency, samples, color in series:
for key, frequency, samples, pen in series:
if not samples.size or frequency.size != samples.size:
continue
active.add(key)
x_min, x_max = min(x_min, float(np.min(frequency))), max(x_max, float(np.max(frequency)))
step = max(1, int(np.ceil(samples.size / self.max_points))) if self.max_points > 0 else 1
if settings.show_magnitude:
magnitude = 20.0 * np.log10(np.maximum(np.abs(samples[::step]), 1e-12))
curve = mag.update_curve(key, frequency[::step], magnitude, color,
width=1.8 if single else 1.4)
magnitude = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
curve = mag.update_curve(key, frequency, magnitude, pen)
mag_sources.setdefault(key[0], curve)
if settings.show_phase:
angles = np.angle(samples)
if settings.unwrap_phase:
angles = np.unwrap(angles)
curve = phase.update_curve(key, frequency[::step], np.degrees(angles[::step]),
"#80ed99" if single else color,
width=1.4 if single else 1.2, dashed=single)
curve = phase.update_curve(key, frequency, np.degrees(angles), pen)
phase_sources.setdefault(key[0], curve)
for panel, visible, sources in ((mag, settings.show_magnitude, mag_sources),
@@ -171,7 +158,6 @@ class TraceRenderer:
panel.sync_legend(sources if not single else {})
if visible:
panel.item.setTitle(title)
panel.view.invertY(False)
if active and np.isfinite(x_min) and np.isfinite(x_max):
panel.view.setXRange(x_min, x_max, padding=0.02)
if settings.show_magnitude: