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
+14 -9
View File
@@ -4,14 +4,13 @@ from __future__ import annotations
from collections import deque
from PyQt6.QtCore import QRectF
import numpy as np
import pyqtgraph as pg
from python_app.models.collection_identity import collection_key
from python_app.models.dataset_model import ResultCollection
from python_app.models.gui_profile_schema import GuiBscanStateModel
from python_app.gui.controllers.renderers.common import build_lut, create_plot
from python_app.gui.controllers.renderers.common import build_lut, create_plot, image_rect
def _result_tail(
@@ -109,16 +108,18 @@ def bscan_levels(sweeps: np.ndarray, axis_mode: str) -> tuple[float, float]:
return -max_abs, max_abs
_TITLE = "B-scan S21"
class BscanRenderer:
"""Own the heatmap and its per-combination arrays; input history stays external."""
def __init__(self) -> None:
self.widget = create_plot()
self.widget = create_plot(_TITLE)
self.widget.setLabel("bottom", "Sweep #")
self.widget.setLabel("left", "Range", units="m")
item = self.widget.getPlotItem()
assert item is not None
item.getViewBox().invertY(False)
item.getViewBox().enableAutoRange(x=False, y=False)
self.image = pg.ImageItem(axisOrder="row-major")
item.addItem(self.image)
@@ -139,24 +140,28 @@ class BscanRenderer:
history = self.history_by_combo.get(key) if key is not None else None
depth = self.depth_axis_by_combo.get(key) if key is not None else None
if not history or depth is None:
self.image.clear()
self._show_empty()
return False
sweeps = apply_mean_ascan_subtraction(history, enabled=settings.subtract_mean_ascan)
depth_min, depth_max = float(np.min(depth)), float(np.max(depth))
count = sweeps.shape[0]
rect = image_rect(np.arange(1, count + 1), depth)
self.image.setImage(sweeps.T, autoLevels=False)
self.image.setRect(QRectF(.5, depth_min, float(count), max(depth_max - depth_min, 1e-6)))
self.image.setRect(rect)
self.image.setLookupTable(bscan_lookup_table(settings.axis))
self.image.setLevels(bscan_levels(sweeps, settings.axis))
item = self.widget.getPlotItem()
assert item is not None
item.getViewBox().setRange(xRange=(.5, .5 + count), yRange=(depth_min, depth_max), padding=.02)
item.getViewBox().setRange(rect, padding=.02)
assert key is not None
self.widget.setTitle(f"B-scan S21 in{key[0]}/out{key[1]} | sweeps={count}")
self.widget.setTitle(f"{_TITLE} in{key[0]}/out{key[1]} | sweeps={count}")
return True
def clear(self) -> None:
self.history_by_combo.clear()
self.depth_axis_by_combo.clear()
self.signature = None
self._show_empty()
def _show_empty(self) -> None:
self.image.clear()
self.widget.setTitle(_TITLE)
+27 -5
View File
@@ -1,15 +1,25 @@
"""Plot construction and color tables shared by radar views."""
"""Plot construction, colors and color tables shared by radar views."""
import numpy as np
import pyqtgraph as pg
from PyQt6.QtCore import QRectF
from python_app.gui.theme import PLOT_FOREGROUND
from python_app.models.run_config_schema import GprModel
from python_app.models.run_config_validation import parse_position_csv
from python_app.gui.theme import PLOT_BACKGROUND
# Series identity colors in fixed order, checked for the white plot surface: lightness band,
# chroma floor and neighbour separation under normal and colorblind vision. Aqua, yellow and
# pink stay under 3:1 contrast with white, so lines are 2 px wide and the legend names pairs.
SERIES_COLORS = ("#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948")
# A captured trace shown on its own (main view, capture preview, PNG export).
CAPTURED_TRACE_PEN = pg.mkPen(SERIES_COLORS[0], width=2)
# Behind legends and labels, so their text stays legible over curves and heatmaps.
OVERLAY_FILL = (255, 255, 255, 200)
def create_plot(title: str = "") -> pg.PlotWidget:
plot = pg.PlotWidget(background=PLOT_BACKGROUND, enableMenu=False, title=title)
plot = pg.PlotWidget(enableMenu=False, title=title)
plot.showGrid(x=True, y=True, alpha=0.2)
return plot
@@ -26,8 +36,20 @@ def build_lut(stops: list[str], *, size: int = 256) -> np.ndarray:
return lut
# Same Tx/Rx markers on ground BP and the three planar SAR views.
ANTENNA_STYLES = (("t", "#ff595e", "#ffca3a"), ("t1", "#4cc9f0", "#e0fbfc"))
def image_rect(x_axis: np.ndarray, y_axis: np.ndarray) -> QRectF:
"""Rectangle covering image pixels centred on the axis samples; a lone sample spans one unit."""
(x, width), (y, height) = (_pixel_span(axis) for axis in (x_axis, y_axis))
return QRectF(x, y, width, height)
def _pixel_span(axis: np.ndarray) -> tuple[float, float]:
first, last = float(axis[0]), float(axis[-1])
pitch = (last - first) / (len(axis) - 1) if len(axis) > 1 else 1.0
return first - pitch / 2, last - first + pitch
# Same Tx/Rx markers (symbol, fill, outline) on ground BP and the three planar SAR views.
ANTENNA_STYLES = (("t", SERIES_COLORS[7], PLOT_FOREGROUND), ("t1", SERIES_COLORS[0], PLOT_FOREGROUND))
def selected_antenna_coordinates(geometry: GprModel, inputs: str, outputs: str) -> tuple[np.ndarray, np.ndarray]:
+33 -61
View File
@@ -6,10 +6,13 @@ from PyQt6.QtCore import QRectF
import numpy as np
import pyqtgraph as pg
from python_app.gui.controllers.renderers.common import ANTENNA_STYLES, build_lut, create_plot, selected_antenna_coordinates
from python_app.gui.controllers.renderers.common import (
ANTENNA_STYLES, OVERLAY_FILL, build_lut, create_plot, image_rect, selected_antenna_coordinates,
)
from python_app.gui.result_payloads import (
collection_payload_by_name, gpr_object_rows,
)
from python_app.gui.theme import PLOT_FOREGROUND
from python_app.models.dataset_model import ResultCollection, ResultPayload
from python_app.models.gui_profile_schema import GuiGprStateModel
from python_app.models.run_config_schema import GprModel
@@ -59,13 +62,16 @@ class GprRenderer:
"""Draw one collection from explicit display settings and antenna geometry."""
def __init__(self) -> None:
self.widget = create_plot("GPR coherent BP — ground plane")
self.widget = create_plot(_title())
plot = self.widget.getPlotItem()
assert plot is not None
self._plot = plot
self._view = plot.getViewBox()
self._plot.showGrid(x=True, y=True, alpha=0.2)
self._configure_axes()
plot.setLabel("bottom", "X", units="m")
plot.setLabel("left", "Range", units="m")
plot.hideButtons()
self._view.enableAutoRange(x=False, y=False)
self._view.setMouseEnabled(x=False, y=False)
self._lookup_table = build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"])
self._image = pg.ImageItem(axisOrder="row-major")
self._tx = pg.ScatterPlotItem()
@@ -80,21 +86,6 @@ class GprRenderer:
self._geometry_signature: tuple[object, ...] | None = None
self._selected_geometry = (np.empty((0, 3)), np.empty((0, 3)))
def _configure_axes(self) -> None:
plot = self._plot
plot.showAxis("left", show=True)
plot.showAxis("bottom", show=True)
plot.setClipToView(True)
plot.setLabel("bottom", "X", units="m")
plot.setLabel("left", "Range", units="m")
plot.setMenuEnabled(False)
plot.hideButtons()
view = self._view
view.invertY(False)
view.enableAutoRange(x=False, y=False)
view.setMouseEnabled(x=False, y=False)
view.setMenuEnabled(False)
def _clear_annotations(self) -> None:
for item in self._labels:
self._plot.removeItem(item)
@@ -102,11 +93,9 @@ class GprRenderer:
def clear(self) -> None:
"""Clear displayed data while retaining reusable graphics items."""
self._configure_axes()
self._clear_annotations()
self._image.hide()
for marker in self._markers:
marker.setData(x=[], y=[])
marker.hide()
self._plot.setTitle(_title())
@@ -128,8 +117,16 @@ class GprRenderer:
brush=pg.mkBrush(brush), pen=pg.mkPen(pen, width=1.0))
marker.setVisible(len(coordinates) > 0)
def _label(self, text: str, x: float, y: float, color: str) -> pg.TextItem:
label = pg.TextItem(text=text, color=color, anchor=(0.0, 1.0))
def _draw_image(self, accumulator: ResultPayload) -> None:
image = np.asarray(accumulator.image, dtype=np.float32)
self._image.setImage(image, autoLevels=False)
self._image.setRect(image_rect(accumulator.image_x_axis, accumulator.image_y_axis))
self._image.setLookupTable(self._lookup_table)
self._image.setLevels((float(np.min(image)), float(np.max(image) + 1e-6)))
self._image.show()
def _label(self, text: str, x: float, y: float) -> pg.TextItem:
label = pg.TextItem(text=text, color=PLOT_FOREGROUND, fill=pg.mkBrush(*OVERLAY_FILL), anchor=(0.0, 1.0))
label.setZValue(40)
label.setPos(x, y)
self._plot.addItem(label)
@@ -153,6 +150,7 @@ class GprRenderer:
self, rows: np.ndarray, objects_only: bool, x_span: float, z_span: float,
) -> None:
if rows.size == 0:
self._points.hide()
return
self._points.setData(
x=rows[:, 0], y=rows[:, 1], symbol="o" if objects_only else "d",
@@ -167,7 +165,7 @@ class GprRenderer:
text = f"{float(score):.2f}"
if objects_only:
text += f" | x={x:.1f} | z={z:.1f}"
label = self._label(text, x, z, "#ffd6d9" if objects_only else "#ffffff")
label = self._label(text, x, z)
if objects_only:
self._place_object_label(label, x, z, x_span, z_span, occupied)
@@ -175,8 +173,6 @@ class GprRenderer:
"""Render a heatmap or object-only view, returning whether data is present."""
objects_only = settings.render_mode == "objects_only"
accumulator = collection_payload_by_name(collection, "gpr_accumulator", kind=3)
x_min, x_max = normalized_display_range(settings.visible_x_min_m, settings.visible_x_max_m)
z_min, z_max = normalized_display_range(settings.visible_z_min_m, settings.visible_z_max_m)
rows = gpr_object_rows(collection)
if objects_only:
if accumulator is None and rows.size == 0:
@@ -186,42 +182,18 @@ class GprRenderer:
self.clear()
return False
self.widget.setUpdatesEnabled(False)
try:
self._clear_annotations()
x_min, x_max = normalized_display_range(settings.visible_x_min_m, settings.visible_x_max_m)
z_min, z_max = normalized_display_range(settings.visible_z_min_m, settings.visible_z_max_m)
self._view.setXRange(x_min, x_max, padding=0.0)
self._view.setYRange(display_y_min(z_min, z_max), z_max, padding=0.0)
self._clear_annotations()
self._draw_geometry(settings, geometry)
if objects_only or accumulator is None:
self._image.hide()
for marker in (self._points,):
marker.setData(x=[], y=[])
marker.hide()
self._view.invertY(False)
self._draw_geometry(settings, geometry)
rect: QRectF | None = None
if not objects_only and accumulator is not None:
image = np.asarray(accumulator.image, dtype=np.float32)
xs = np.asarray(accumulator.image_x_axis, dtype=np.float32)
ys = np.asarray(accumulator.image_y_axis, dtype=np.float32)
rect = QRectF(
float(xs[0]), float(ys[0]),
max(float(xs[-1]) - float(xs[0]), 1e-6),
max(float(ys[-1]) - float(ys[0]), 1e-6),
)
self._image.setImage(image, autoLevels=False)
self._image.setRect(rect)
self._image.setLookupTable(self._lookup_table)
self._image.setLevels((float(np.min(image)), float(np.max(image) + 1e-6)))
self._image.show()
self._view.setXRange(x_min, x_max, padding=0.0)
self._view.setYRange(display_y_min(z_min, z_max), z_max, padding=0.0)
points = collection_payload_by_name(collection, "gpr_points", kind=4)
if points is None or np.asarray(points.table).size == 0:
rows = np.zeros((0, 3), dtype=np.float32)
self._draw_points(rows, objects_only, x_max - x_min, z_max - z_min)
if objects_only:
self._view.setXRange(x_min, x_max, padding=0.0)
self._view.setYRange(display_y_min(z_min, z_max), z_max, padding=0.0)
self._plot.setTitle(_title(objects_only=objects_only))
finally:
self.widget.setUpdatesEnabled(True)
else:
self._draw_image(accumulator)
self._draw_points(rows, objects_only, x_max - x_min, z_max - z_min)
self._plot.setTitle(_title(objects_only=objects_only))
return True
@staticmethod
+20 -25
View File
@@ -24,8 +24,11 @@ import pyqtgraph as pg
from PyQt6.QtCore import QRectF, Qt
from PyQt6.QtWidgets import QGridLayout, QLabel, QVBoxLayout, QWidget
from python_app.gui.controllers.renderers.common import ANTENNA_STYLES, build_lut, create_plot, selected_antenna_coordinates
from python_app.gui.controllers.renderers.common import (
ANTENNA_STYLES, build_lut, create_plot, image_rect, selected_antenna_coordinates,
)
from python_app.gui.result_payloads import collection_payload_by_name
from python_app.gui.theme import PLOT_FOREGROUND
from python_app.models.dataset_model import ResultCollection, ResultPayload
from python_app.models.gui_profile_schema import GuiSar3dStateModel
from python_app.models.run_config_schema import GprModel
@@ -33,6 +36,9 @@ from python_app.models.run_config_schema import GprModel
# Status codes published by the sar3d processor in its "sar3d_status" table.
SAR3D_STATES = {0: "ready", 1: "no signal", 2: "error", 3: "warning"}
_GROUND_COLOR = (115, 105, 88)
_SCENE_BOX_COLOR = "#898781"
# payload -> (title, x label, y label, scene axes (0 = X, 1 = Y, 2 = Z) along the plot axes)
SAR3D_VIEWS: dict[str, tuple[str, str, str, tuple[int, int] | None]] = {
"sar3d_front": ("Front (X-Y)", "X", "Y", (0, 1)),
@@ -61,13 +67,6 @@ def sar3d_status(collection: ResultCollection) -> tuple[int, str] | None:
return code, bytes(row[1:].astype(np.uint8)).decode("utf-8", errors="replace")
def _image_rect(x_axis: np.ndarray, y_axis: np.ndarray) -> QRectF:
"""Rectangle covering the pixels centred on the axis samples."""
dx = (x_axis[-1] - x_axis[0]) / max(len(x_axis) - 1, 1)
dy = (y_axis[-1] - y_axis[0]) / max(len(y_axis) - 1, 1)
return QRectF(x_axis[0] - dx / 2, y_axis[0] - dy / 2, x_axis[-1] - x_axis[0] + dx, y_axis[-1] - y_axis[0] + dy)
class Sar3dRenderer:
"""Draws the 3D SAR views."""
@@ -85,21 +84,19 @@ class Sar3dRenderer:
grid_layout.setContentsMargins(0, 0, 0, 0)
grid_layout.setSpacing(6)
layout.addWidget(self.grid, stretch=1)
self.plots: dict[str, pg.PlotWidget] = {}
for index, (name, (title, *_rest)) in enumerate(SAR3D_VIEWS.items()):
plot = create_plot(title)
grid_layout.addWidget(plot, index // 2, index % 2)
self.plots[name] = plot
lut = build_lut(["#081c15", "#1b4332", "#ffd166", "#f94144"]) # the GPR palette
lut = np.column_stack([lut, np.full(len(lut), 255, dtype=np.uint8)])
lut[0, 3] = 0 # below SAR_CLOUD_THRESHOLD
self.plots: dict[str, pg.PlotWidget] = {}
self.antennas: dict[str, tuple[pg.ScatterPlotItem, pg.ScatterPlotItem]] = {}
self.images: dict[str, pg.ImageItem] = {}
self.targets: dict[str, pg.PlotCurveItem] = {}
for name, (_title, x_label, y_label, _axes) in SAR3D_VIEWS.items():
plot = self.plots[name]
for index, (name, (title, x_label, y_label, axes)) in enumerate(SAR3D_VIEWS.items()):
plot = create_plot(title)
grid_layout.addWidget(plot, index // 2, index % 2)
self.plots[name] = plot
plot.setAspectLocked(True)
if _axes is not None:
if axes is not None:
markers = (pg.ScatterPlotItem(), pg.ScatterPlotItem())
for marker in markers:
marker.setZValue(4)
@@ -109,42 +106,40 @@ class Sar3dRenderer:
assert item is not None
view = item.getViewBox()
view.setMouseEnabled(x=False, y=False)
view.setMenuEnabled(False)
view.enableAutoRange(x=False, y=False)
item.hideButtons()
image = pg.ImageItem(axisOrder="row-major", lut=lut)
plot.addItem(image)
# Hollow, so the response at the maximum stays visible.
peak = pg.PlotCurveItem(pen=pg.mkPen("w", width=1.5))
peak = pg.PlotCurveItem(pen=pg.mkPen(PLOT_FOREGROUND, width=1.5))
peak.setZValue(3)
plot.addItem(peak)
self.images[name] = image
self.targets[name] = peak
if x_label:
plot.showGrid(x=True, y=True, alpha=0.2)
plot.setLabel("bottom", x_label, units="m")
plot.setLabel("left", y_label, units="m")
if y_label == "Y":
ground = pg.InfiniteLine(pos=0.0, angle=0, pen=pg.mkPen("w", width=1, style=Qt.PenStyle.DashLine))
ground = pg.InfiniteLine(pos=0.0, angle=0,
pen=pg.mkPen(_GROUND_COLOR, width=1, style=Qt.PenStyle.DashLine))
ground.setZValue(2)
plot.addItem(ground)
oblique = self.plots["sar3d_oblique"]
oblique.hideAxis("bottom")
oblique.hideAxis("left")
oblique.setAspectLocked(True)
# The notebook scene's ground, under the image so it shows through weak voxels.
self.ground = pg.PlotCurveItem(
pen=pg.mkPen(115, 105, 88), brush=pg.mkBrush(115, 105, 88, 110), fillLevel="enclosed"
pen=pg.mkPen(_GROUND_COLOR), brush=pg.mkBrush(*_GROUND_COLOR, 110), fillLevel="enclosed"
)
self.ground.setZValue(-1)
oblique.addItem(self.ground)
self.box = pg.PlotCurveItem(pen=pg.mkPen(190, 190, 190, 170), connect="pairs")
self.box = pg.PlotCurveItem(pen=pg.mkPen(_SCENE_BOX_COLOR), connect="pairs")
self.box.setZValue(2)
oblique.addItem(self.box)
self.axis_labels: dict[str, pg.TextItem] = {}
for axis_name in _AXIS_EDGES:
label = pg.TextItem(axis_name, color=(190, 190, 190), anchor=(0.5, 0.5))
label = pg.TextItem(axis_name, color=PLOT_FOREGROUND, anchor=(0.5, 0.5))
oblique.addItem(label)
self.axis_labels[axis_name] = label
self.clear()
@@ -195,7 +190,7 @@ class Sar3dRenderer:
zoom = settings.zoom_percent / 100.0
for name, payload in payloads.items():
title, _x_label, _y_label, axes = SAR3D_VIEWS[name]
rect = _image_rect(payload.image_x_axis, payload.image_y_axis)
rect = image_rect(payload.image_x_axis, payload.image_y_axis)
image = self.images[name]
image.setImage(np.where(payload.image >= settings.cloud_threshold, payload.image, 0),
autoLevels=False, levels=(0, 1))
+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: