From e5a3d327e5bf37c1acac9ec72f5d0b0cad1fb927 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 28 Apr 2026 14:08:44 -0400 Subject: [PATCH] refactor: unify signal viewer styling and update docs screenshots - Align view_simple and view_full on background colour (#161616), title size (25pt), subtitle size (15pt), base font/tick/label sizes, grid style (alpha=0.2), and legend fontsize (10pt) - Spectrogram placed above IQ plot in view_simple; subplot renamed from "Time Series" to "IQ Sample Plot" - Frequency and spectrogram Y-axes formatted in MHz across both viewers - Added xlabel/ylabel, subtle grids, and IQ legend to view_full subplots - Fixed spectrogram right-side clipping in view_simple by syncing xlim from specgram output rather than total signal duration - Updated getting_started.rst to reference both simple and full viewer screenshots; replaced doc images with latest renders --- docs/source/intro/getting_started.rst | 8 +- src/ria_toolkit_oss/view/view_signal.py | 50 +++++++---- .../view/view_signal_simple.py | 83 ++++++++++--------- 3 files changed, 88 insertions(+), 53 deletions(-) diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index b07f1a3..2dd4b6a 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -414,12 +414,18 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria view capture.npy --show --no-save ria view old.npy --legacy --type simple ria view recordings\qam64_35.npy --type simple + ria view recordings\qam64_35.npy --type full -.. figure:: ../images/qam64_35.png +.. figure:: ../images/recordings/qam64_35.png :alt: Example output of ria view recordings\qam64_35.npy --type simple Output of ``ria view recordings\qam64_35.npy --type simple`` +.. figure:: ../images/recordings/qam64_35-full.png + :alt: Example output of ria view recordings\qam64_35.npy --type full + + Output of ``ria view recordings\qam64_35.npy --type full`` + .. _cmd-annotate: diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index a059e60..8c15fad 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -3,11 +3,12 @@ import os import textwrap from typing import Optional +import matplotlib import matplotlib.pyplot as plt import numpy as np -from matplotlib import gridspec +from matplotlib import gridspec, ticker from matplotlib.patches import Patch -from PIL import Image +from PIL import Image, UnidentifiedImageError from scipy.fft import fft, fftshift from scipy.signal import spectrogram from scipy.signal.windows import hann @@ -185,7 +186,7 @@ def view_sig( logo: Optional[bool] = True, dark: Optional[bool] = True, spines: Optional[bool] = False, - title_fontsize: Optional[int] = 35, + title_fontsize: Optional[int] = 25, subtitle_fontsize: Optional[int] = 15, ) -> None: """ @@ -230,11 +231,24 @@ def view_sig( complex_signal = recording.data[0] sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata) - subplot_height = 2 * (plot_spectrogram + iq + frequency) + 3 * (constellation or metadata or logo) + subplot_height = 3 * (plot_spectrogram) + 2 * (iq + frequency) + 3 * (constellation or metadata or logo) subplot_width = max((constellation + metadata or 1), logo * 3) if dark: plt.style.use("dark_background") + matplotlib.rcParams.update({ + "figure.facecolor": "#161616", + "axes.facecolor": "#161616", + "savefig.facecolor": "#161616", + "savefig.edgecolor": "#161616", + "font.size": 10, + "axes.titlesize": 15, + "axes.labelsize": 10, + "xtick.labelsize": 10, + "ytick.labelsize": 10, + "legend.frameon": False, + "legend.facecolor": "none", + }) logo_path = os.path.dirname(__file__) + "/graphics/Qoherent-logo-white-transparent.png" else: plt.style.use("default") @@ -252,8 +266,8 @@ def view_sig( plot_x_indx = 0 if plot_spectrogram: - spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) - plot_y_indx = plot_y_indx + 2 + spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 3, :]) + plot_y_indx = plot_y_indx + 3 fft_size = get_fft_size(plot_length=plot_length) _, t_spec, Sxx = spectrogram( @@ -280,7 +294,12 @@ def view_sig( ) set_spines(spec_ax, spines) - spec_ax.set_title("Spectrogram", loc="center", fontsize=subtitle_fontsize) + spec_ax.set_title("Spectrogram", loc="left", fontsize=subtitle_fontsize) + spec_ax.set_xlabel("Time (s)") + spec_ax.set_ylabel("Frequency (MHz)") + spec_ax.yaxis.set_major_formatter( + ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}") + ) if iq: iq_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) @@ -291,12 +310,13 @@ def view_sig( iq_ax.plot(t, plot_iq.real, color=COLORS["purple"], linewidth=0.6, alpha=0.8, label="I") iq_ax.plot(t, plot_iq.imag, color=COLORS["magenta"], linewidth=0.6, alpha=0.8, label="Q") - iq_ax.grid(False) + iq_ax.grid(True, alpha=0.2, linewidth=0.5) iq_ax.set_ylabel("Amplitude") iq_ax.set_xlim([min(t), max(t)]) iq_ax.set_xlabel("Time (s)") - iq_ax.set_title("IQ Sample Plot", fontsize=subtitle_fontsize) + iq_ax.set_title("IQ Sample Plot", loc="left", fontsize=subtitle_fontsize) + iq_ax.legend(loc="upper right", fontsize=10) set_spines(iq_ax, spines) if frequency: @@ -310,10 +330,12 @@ def view_sig( # Convert to dB spectrum_db = 20 * np.log10(spectrum + 1e-12) # 20*log for magnitude - freqs = np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal[:plot_length])) + center_frequency + freqs = (np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal[:plot_length])) + center_frequency) / 1e6 freq_ax.plot(freqs, spectrum_db, color=COLORS["accent"], linewidth=0.8) + freq_ax.set_xlabel("Frequency (MHz)") freq_ax.set_ylabel("Magnitude (dB)") - freq_ax.set_title("Frequency Spectrum (Windowed FFT)", fontsize=subtitle_fontsize) + freq_ax.grid(True, alpha=0.2, linewidth=0.5) + freq_ax.set_title("Frequency Spectrum (Windowed FFT)", loc="left", fontsize=subtitle_fontsize) set_spines(freq_ax, spines) if constellation: @@ -326,7 +348,7 @@ def view_sig( const_ax.set_ylim([-1 * dimension, dimension]) const_ax.set_xlabel("In-phase (I)") const_ax.set_ylabel("Quadrature (Q)") - const_ax.set_title("Constellation", fontsize=subtitle_fontsize) + const_ax.set_title("Constellation", loc="left", fontsize=subtitle_fontsize) const_ax.set_aspect("equal") if not spines: @@ -375,8 +397,8 @@ def view_sig( image = Image.open(logo_path) # Open the PNG image using PIL logo_ax.imshow(image) - except FileNotFoundError: - print(f"Warning, {logo_path} not found.") + except (FileNotFoundError, UnidentifiedImageError, OSError) as exc: + print(f"Warning, could not load logo image: {logo_path}. Reason: {exc}") fig.subplots_adjust( left=0.1, # Left margin diff --git a/src/ria_toolkit_oss/view/view_signal_simple.py b/src/ria_toolkit_oss/view/view_signal_simple.py index c770b5a..a3bb280 100644 --- a/src/ria_toolkit_oss/view/view_signal_simple.py +++ b/src/ria_toolkit_oss/view/view_signal_simple.py @@ -119,24 +119,19 @@ def setup_style(*, labels_mode: bool = False, compact_mode: bool = False) -> Non label_font = 14 else: base_font = 10 - title_font = 12 + title_font = 15 label_font = 10 matplotlib.rcParams.update( { - "figure.facecolor": "#0f172a", - "axes.facecolor": "#1e293b", - "axes.edgecolor": COLORS["muted"], - "axes.labelcolor": COLORS["light"], - "text.color": COLORS["light"], - "xtick.color": COLORS["muted"], - "ytick.color": COLORS["muted"], - "grid.color": COLORS["muted"], - "grid.alpha": 0.3, + "figure.facecolor": "#161616", + "axes.facecolor": "#161616", + "savefig.facecolor": "#161616", + "savefig.edgecolor": "#161616", "font.size": base_font, "axes.titlesize": title_font, "axes.labelsize": label_font, - "figure.titlesize": title_font + 2, + "figure.titlesize": title_font + 4, "legend.frameon": False, "legend.facecolor": "none", "xtick.labelsize": base_font, @@ -194,7 +189,7 @@ def view_simple_sig( constellation_mode: Optional[bool] = False, labels_mode: Optional[bool] = False, slice: Optional[tuple] = None, - title: Optional[str] = "Signal", + title: Optional[str] = "Signal Plot", ): """ Create a simple plot of various signal visualizations as a png or svg image. @@ -237,7 +232,7 @@ def view_simple_sig( spec_signal = signal if compact_mode: - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={"height_ratios": [1, 5]}) + fig, (ax2, ax1) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={"height_ratios": [5, 1]}) show_title = False show_labels = False ax_constellation = ax_psd = None @@ -253,25 +248,24 @@ def view_simple_sig( ax_psd = None else: if constellation_mode: - fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12)) + fig, ((ax2, ax1), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12)) ax_constellation, ax_psd = ax3, ax4 else: - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10)) + fig, (ax2, ax1) = plt.subplots(2, 1, figsize=(14, 10)) ax_constellation = ax_psd = None show_title = True show_labels = labels_mode if show_title: - fig.suptitle(title, fontsize=16, color=COLORS["light"], y=0.96) - fig.patch.set_facecolor("#0f172a") + fig.suptitle(title, fontsize=25) + fig.patch.set_facecolor(matplotlib.rcParams["figure.facecolor"]) total_duration_s = len(signal) / sample_rate_hz if sample_rate_hz else 0.0 t_s = np.linspace(0, total_duration_s, len(display_signal)) if len(display_signal) else np.array([]) - ax1.plot(t_s, display_signal.real, color=COLORS["purple"], linewidth=0.8, alpha=0.8, label="I") - ax1.plot(t_s, display_signal.imag, color=COLORS["magenta"], linewidth=0.8, alpha=0.8, label="Q") - ax1.set_xlim(0, total_duration_s) - ax1.grid(True, alpha=0.3) + ax1.plot(t_s, display_signal.real, color=COLORS["purple"], linewidth=0.6, alpha=0.8, label="I") + ax1.plot(t_s, display_signal.imag, color=COLORS["magenta"], linewidth=0.6, alpha=0.8, label="Q") + ax1.grid(True, alpha=0.2, linewidth=0.5) nfft, overlap = _get_nfft_size(signal=signal, fast_mode=fast_mode) @@ -285,7 +279,7 @@ def view_simple_sig( ) ax2.set_ylim(center_freq_hz - sample_rate_hz / 2, center_freq_hz + sample_rate_hz / 2) - ax2.set_xlim(0, total_duration_s) + ax1.set_xlim(ax2.get_xlim()) if show_labels: if horizontal_mode: @@ -294,20 +288,26 @@ def view_simple_sig( ax2.set_xlabel("Time (s)") ax1.set_ylabel("Amplitude") - ax1.set_title(f"Time Series - {sdr} SDR", loc="left", pad=10) - ax1.legend(loc="upper right") + ax1.set_title(f"IQ Sample Plot - {sdr} SDR", loc="left", pad=10, fontsize=15) + ax1.legend(loc="upper right", fontsize=10) - ax2.set_ylabel("Frequency (Hz)") + ax2.set_ylabel("Frequency (MHz)") ax2.set_title( - f"Spectrogram - {center_freq_hz / 1e6:.1f} MHz ± {sample_rate_hz / 2e6:.1f} MHz", loc="left", pad=10 + f"Spectrogram - {center_freq_hz / 1e6:.1f} MHz ± {sample_rate_hz / 2e6:.1f} MHz", loc="left", pad=10, fontsize=15 + ) + ax2.yaxis.set_major_formatter( + matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}") ) - yticks = ax2.get_yticks() - ax2.set_yticklabels([f"{y / 1e6:.1f}" for y in yticks]) elif not compact_mode: - ax1.set_title("Time Series", loc="left", pad=10) - ax1.legend(loc="upper right", fontsize=8) + ax1.set_title("IQ Sample Plot", loc="left", pad=10, fontsize=15) + ax1.legend(loc="upper right", fontsize=10) - ax2.set_title("Spectrogram", loc="left", pad=10) + ax2.set_xlabel("Time (s)") + ax2.set_ylabel("Frequency (MHz)") + ax2.set_title("Spectrogram", loc="left", pad=10, fontsize=15) + ax2.yaxis.set_major_formatter( + matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}") + ) _add_annotations( annotations=annotations, @@ -339,8 +339,8 @@ def view_simple_sig( ) ax_constellation.set_xlabel("In-phase (I)") ax_constellation.set_ylabel("Quadrature (Q)") - ax_constellation.set_title("Constellation") - ax_constellation.grid(True, alpha=0.3) + ax_constellation.set_title("Constellation", loc="left", fontsize=15) + ax_constellation.grid(True, alpha=0.2, linewidth=0.5) ax_constellation.set_aspect("equal") if ax_psd is not None: @@ -351,11 +351,11 @@ def view_simple_sig( freqs = freqs + center_freq_hz spectrum_db = 10 * np.log10(spectrum + 1e-12) - ax_psd.plot(freqs / 1e6, spectrum_db, color=COLORS["accent"], linewidth=1.0) + ax_psd.plot(freqs / 1e6, spectrum_db, color=COLORS["accent"], linewidth=0.8) ax_psd.set_xlabel("Frequency (MHz)") ax_psd.set_ylabel("Power (dB)") - ax_psd.set_title("Power Spectral Density") - ax_psd.grid(True, alpha=0.3) + ax_psd.set_title("Power Spectral Density", loc="left", fontsize=15) + ax_psd.grid(True, alpha=0.2, linewidth=0.5) if compact_mode: ax1.set_xticks([]) @@ -367,13 +367,20 @@ def view_simple_sig( else: plt.tight_layout() if show_title: - plt.subplots_adjust(top=0.92) + plt.subplots_adjust(top=0.9) if saveplot: output_path, extension = set_path(output_path=output_path) dpi_value = _set_dpi(fast_mode=fast_mode, labels_mode=labels_mode, extension=extension) - plt.savefig(output_path, dpi=dpi_value, bbox_inches="tight", facecolor="#0f172a", edgecolor="none") + plt.savefig( + output_path, + dpi=dpi_value, + bbox_inches="tight", + pad_inches=0.3, + facecolor=matplotlib.rcParams["savefig.facecolor"], + edgecolor=matplotlib.rcParams["savefig.edgecolor"], + ) print(f"Saved signal plot to {output_path}") return output_path