Skip to content

plot_phase

yohou.plotting.signal.plot_phase(df, *, columns=None, unwrap=True, angle_unit='radian', groups=None, facet_by='member', facet_n_cols=2, color_palette=None, show_legend=True, title=None, x_label=None, y_label=None, width=None, height=None, connect_gaps=False, line_width=2.0)

Plot the phase of a time series.

Shows the phase angle of each frequency component computed via FFT. Useful for understanding temporal alignment of periodic patterns.

Parameters

Name Type Description Default
df DataFrame

Input DataFrame with 'time' column and numeric columns.

required
columns str | list[str] | None

Column(s) to analyze. If None, uses all numeric columns except 'time'.

None
unwrap bool

Unwrap phase angles to avoid discontinuities at :math:\\pm\\pi.

True
angle_unit Literal['degree', 'radian']

Unit for the phase angle axis.

"radian"
groups list[str] | None

Panel group prefixes to plot.

None
facet_by Literal['group', 'member'] | None

Faceting axis for panel data. "group" creates one subplot per group, "member" one per member. None disables faceting. Ignored for non-panel data.

"member"
facet_n_cols int

Number of columns in facet grid.

2
color_palette list[str] | None

Custom color palette.

None
show_legend bool

Whether to show the legend.

True
title str | None

Plot title.

None
x_label str | None

X-axis label. Defaults to "Frequency (cycles/sample)".

None
y_label str | None

Y-axis label. Defaults to "Phase (radians)" or "Phase (degrees)".

None
width int | None

Plot width in pixels.

None
height int | None

Plot height in pixels.

None
connect_gaps bool

Whether to connect gaps in the data with lines.

False
line_width float

Width of the line traces.

2.0

Returns

Type Description
Figure

Plotly figure object.

Examples

>>> import polars as pl
>>> import numpy as np
>>> from yohou.plotting.signal import plot_phase
>>> t = np.arange(100)
>>> y = np.sin(2 * np.pi * 0.1 * t) + 0.5 * np.sin(2 * np.pi * 0.25 * t)
>>> df = pl.DataFrame({
...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 4, 9), "1d", eager=True),
...     "y": y,
... })
>>> fig = plot_phase(df, columns="y")
>>> len(fig.data) > 0
True

See Also

plot_spectrum : Plot power spectral density.

Source Code

Show/Hide source
def plot_phase(
    df: pl.DataFrame,
    *,
    columns: str | list[str] | None = None,
    unwrap: bool = True,
    angle_unit: Literal["degree", "radian"] = "radian",
    groups: list[str] | None = None,
    facet_by: Literal["group", "member"] | None = "member",
    facet_n_cols: int = 2,
    color_palette: list[str] | None = None,
    show_legend: bool = True,
    title: str | None = None,
    x_label: str | None = None,
    y_label: str | None = None,
    width: int | None = None,
    height: int | None = None,
    connect_gaps: bool = False,
    line_width: float = 2.0,
) -> go.Figure:
    r"""Plot the phase of a time series.

    Shows the phase angle of each frequency component computed via FFT.
    Useful for understanding temporal alignment of periodic patterns.

    Parameters
    ----------
    df : pl.DataFrame
        Input DataFrame with 'time' column and numeric columns.
    columns : str | list[str] | None, default=None
        Column(s) to analyze. If None, uses all numeric columns except 'time'.
    unwrap : bool, default=True
        Unwrap phase angles to avoid discontinuities at :math:`\\pm\\pi`.
    angle_unit : Literal["degree", "radian"], default="radian"
        Unit for the phase angle axis.
    groups : list[str] | None, default=None
        Panel group prefixes to plot.
    facet_by : Literal["group", "member"] | None, default="member"
        Faceting axis for panel data.  ``"group"`` creates one subplot per
        group, ``"member"`` one per member.  ``None`` disables faceting.
        Ignored for non-panel data.
    facet_n_cols : int, default=2
        Number of columns in facet grid.
    color_palette : list[str] | None, default=None
        Custom color palette.
    show_legend : bool, default=True
        Whether to show the legend.
    title : str | None, default=None
        Plot title.
    x_label : str | None, default=None
        X-axis label.  Defaults to "Frequency (cycles/sample)".
    y_label : str | None, default=None
        Y-axis label.  Defaults to "Phase (radians)" or "Phase (degrees)".
    width : int | None, default=None
        Plot width in pixels.
    height : int | None, default=None
        Plot height in pixels.
    connect_gaps : bool, default=False
        Whether to connect gaps in the data with lines.
    line_width : float, default=2.0
        Width of the line traces.

    Returns
    -------
    go.Figure
        Plotly figure object.

    Examples
    --------
    >>> import polars as pl
    >>> import numpy as np
    >>> from yohou.plotting.signal import plot_phase

    >>> t = np.arange(100)
    >>> y = np.sin(2 * np.pi * 0.1 * t) + 0.5 * np.sin(2 * np.pi * 0.25 * t)
    >>> df = pl.DataFrame({
    ...     "time": pl.date_range(pl.date(2020, 1, 1), pl.date(2020, 4, 9), "1d", eager=True),
    ...     "y": y,
    ... })

    >>> fig = plot_phase(df, columns="y")
    >>> len(fig.data) > 0
    True

    See Also
    --------
    [`plot_spectrum`][yohou.plotting.plot_spectrum] : Plot power spectral density.
    """
    use_degrees = angle_unit == "degree"
    unit = "degrees" if use_degrees else "radians"
    validate_plotting_data(df)
    validate_plotting_params(width=width, height=height)

    # Auto-detect panel data
    if groups is None and columns is None and _auto_detect_panel(df):
        groups = []

    if groups is not None:
        _panel_cols = resolve_panel_columns(df, groups, columns)
        _panel_colors = resolve_color_palette(color_palette, len(_panel_cols))
        _legend_tracker = LegendTracker()

        def _render_phase(ctx: RenderContext) -> None:
            """Render phase spectrum for a single panel group."""
            base = [c for c in ctx.sub_df.columns if c != "time"][0]
            y_arr = ctx.sub_df[base].to_numpy().astype(float)
            spectrum = np.fft.rfft(y_arr)
            freqs = np.fft.rfftfreq(len(y_arr))
            phase = np.angle(spectrum)
            if unwrap:
                phase = np.unwrap(phase)
            if use_degrees:
                phase = np.degrees(phase)
            ctx.fig.add_trace(
                go.Scatter(
                    x=freqs[1:].tolist(),
                    y=phase[1:].tolist(),
                    mode="lines",
                    line={"width": line_width, "color": _panel_colors[ctx.entity_idx]},
                    name=ctx.display_name,
                    legendgroup=ctx.display_name,
                    showlegend=_legend_tracker.should_show(ctx.display_name),
                    connectgaps=connect_gaps,
                ),
                row=ctx.row,
                col=ctx.col,
            )

        effective_facet_by = facet_by or "member"
        fig = facet_figure(
            df,
            _render_phase,
            groups=groups,
            columns=columns,
            facet_n_cols=facet_n_cols,
            facet_by=effective_facet_by,
            title=title or "Phase Spectrum",
            x_label=x_label or "Frequency (cycles/sample)",
            y_label=y_label or f"Phase ({unit})",
            width=width,
            height=height,
            shared_xaxes=False,
        )
        fig.update_layout(showlegend=show_legend)
        return fig

    # Non-panel case: column-mode facet_figure
    plot_columns = validate_plotting_data(df, columns=columns, exclude=["time"])
    _colors = resolve_color_palette(color_palette, len(plot_columns))
    _col_colors = dict(zip(plot_columns, _colors, strict=False))
    unit = "degrees" if use_degrees else "radians"

    def _render_phase(ctx: RenderContext) -> None:
        """Render phase spectrum for one column into a subplot."""
        base = ctx.display_name
        col_color = _col_colors[base]
        y = ctx.sub_df[base].to_numpy().astype(float)
        spectrum = np.fft.rfft(y)
        freqs = np.fft.rfftfreq(len(y))
        phase = np.angle(spectrum)
        if unwrap:
            phase = np.unwrap(phase)
        if use_degrees:
            phase = np.degrees(phase)
        freqs = freqs[1:]
        phase = phase[1:]
        unit_label = "\u00b0" if use_degrees else "rad"
        ctx.fig.add_trace(
            go.Scatter(
                x=freqs,
                y=phase,
                mode="lines",
                line={"width": line_width, "color": col_color},
                name=base,
                connectgaps=connect_gaps,
                hovertemplate=(
                    f"<b>{base}</b><br>Frequency: %{{x:.4f}}<br>Phase: %{{y:.2f}} {unit_label}<extra></extra>"
                ),
            ),
            row=ctx.row,
            col=ctx.col,
        )

    fig = facet_figure(
        df,
        _render_phase,
        columns=plot_columns,
        facet_n_cols=facet_n_cols,
        title=title or "Phase Spectrum",
        x_label=x_label or "Frequency (cycles/sample)",
        y_label=y_label or f"Phase ({unit})",
        width=width,
        height=height,
        shared_xaxes=False,
    )
    fig.update_layout(showlegend=show_legend)

    return fig

Tutorials

The following example notebooks use this component:

  • How to Visualize Signal Processing


    Visualization

    Butterworth low-pass filtering with frequency spectrum analysis and phase shift inspection on half-hourly electricity demand data using Plotly.

    View · Open in marimo