Source code for suboptimumg.plotting.two_panel_spectrum

import numpy as np
import plotly.colors as pc
import plotly.graph_objects as go
from numpy.typing import NDArray
from plotly.subplots import make_subplots

from .plotting_constants import (
    DEFAULT_FONT_CONFIG,
    DEFAULT_LAYOUT_CONFIG,
    LINE_WIDTH,
    TEXT_COLOR_DARK,
    FontConfig,
    LayoutConfig,
)


[docs] def plot_two_panel_spectrum( freqs: NDArray[np.float64], magnitude_by_series: dict[str, NDArray[np.float64]], phase_by_series: dict[str, NDArray[np.float64]], *, title: str = "Frequency response", magnitude_label: str = "|H| (dB)", phase_label: str = "phase (deg)", freq_label: str = "frequency (Hz)", log_freq: bool = True, dashed_series: set[str] | None = None, font_config: FontConfig = DEFAULT_FONT_CONFIG, layout_config: LayoutConfig = DEFAULT_LAYOUT_CONFIG, ) -> go.Figure: """Generic stacked magnitude-over-phase (Bode-style) plot. Two vertically stacked panels share a frequency x-axis: magnitude on top, phase below. Each series is drawn in both panels and linked by a single legend entry, so toggling a series hides both its magnitude and phase curves. Parameters ---------- freqs : NDArray[float64] Shared frequency axis (Hz). magnitude_by_series : dict[str, NDArray[float64]] Magnitude curve per series label. Values are plotted verbatim, so pass dB (or linear) already converted. phase_by_series : dict[str, NDArray[float64]] Phase curve (deg) per series label; keys must match ``magnitude_by_series``. title : str, optional Figure title. magnitude_label : str, optional Y-axis label for the magnitude panel. phase_label : str, optional Y-axis label for the phase panel. freq_label : str, optional Shared x-axis label. log_freq : bool, optional Use a logarithmic frequency axis. Default is True. dashed_series : set[str], optional Series labels to render dashed (e.g. a comparison model). Default is None (all solid). font_config : FontConfig, optional layout_config : LayoutConfig, optional Returns ------- go.Figure """ dashed_series = dashed_series or set() palette = pc.qualitative.Plotly fig = make_subplots( rows=2, cols=1, shared_xaxes=True, subplot_titles=(magnitude_label, phase_label), vertical_spacing=layout_config.grid_vertical_spacing, ) for i, (label, mag) in enumerate(magnitude_by_series.items()): color = palette[i % len(palette)] dash = "dash" if label in dashed_series else "solid" group = f"series_{label}" fig.add_trace( go.Scattergl( x=freqs, y=mag, mode="lines", name=label, line=dict(color=color, dash=dash, width=LINE_WIDTH), legendgroup=group, ), row=1, col=1, ) fig.add_trace( go.Scattergl( x=freqs, y=phase_by_series[label], mode="lines", name=label, line=dict(color=color, dash=dash, width=LINE_WIDTH), legendgroup=group, showlegend=False, ), row=2, col=1, ) axis_type = "log" if log_freq else "linear" fig.update_xaxes(type=axis_type, row=1, col=1) fig.update_xaxes(type=axis_type, title_text=freq_label, row=2, col=1) fig.update_yaxes(title_text=magnitude_label, row=1, col=1) fig.update_yaxes(title_text=phase_label, row=2, col=1) fig.update_layout( title={ "text": title, "font": dict(size=font_config.large, color=TEXT_COLOR_DARK), "x": layout_config.title_x, "xanchor": layout_config.title_xanchor, }, height=layout_config.height, width=layout_config.width, margin=layout_config.margin, plot_bgcolor=layout_config.plot_bgcolor, legend=dict(font=dict(size=font_config.small)), ) return fig