"""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 # 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(enableMenu=False, title=title) plot.showGrid(x=True, y=True, alpha=0.2) return plot def build_lut(stops: list[str], *, size: int = 256) -> np.ndarray: """Interpolate hex color stops into 8-bit RGB LUT array.""" stop_positions = np.linspace(0.0, 1.0, num=len(stops), dtype=np.float32) sample_positions = np.linspace(0.0, 1.0, num=size, dtype=np.float32) stop_colors = np.asarray([pg.mkColor(value).getRgb()[:3] for value in stops], dtype=np.float32) lut = np.empty((size, 3), dtype=np.uint8) for channel in range(3): lut[:, channel] = np.interp(sample_positions, stop_positions, stop_colors[:, channel]).astype(np.uint8) return lut 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]: selected_inputs, selected_outputs = parse_position_csv(inputs), parse_position_csv(outputs) tx = [[row.x_m, row.y_m, row.z_m] for row in geometry.tx_geometry if not selected_outputs or row.output_pos in selected_outputs] rx = [[row.x_m, row.y_m, row.z_m] for row in geometry.rx_geometry if not selected_inputs or row.input_pos in selected_inputs] return np.asarray(tx, dtype=float).reshape(-1, 3), np.asarray(rx, dtype=float).reshape(-1, 3)