Skip to content

ISO Plot

soundscapy.plotting.iso_plot

Main module for creating circumplex plots using different backends.

Examples:

>>> from soundscapy import isd, surveys
>>> from soundscapy.plotting.iso_plot import ISOPlot
>>> df = isd.load()
>>> df = surveys.add_iso_coords(df)
>>> sub_df = isd.select_location_ids(df, ['CamdenTown', 'RegentsParkJapan'])
>>> isoplot = (
...    ISOPlot(data=sub_df, hue="SessionID")
...    .create_subplots(
...        subplot_by="LocationID",
...        auto_allocate_axes=True,
...        adjust_figsize=True
...    )
...    .add_scatter()
...    .add_simple_density(fill=False)
...    .style()
... )
>>> isoplot.show()
CLASS DESCRIPTION
ExperimentalWarning

A warning class to signify experimental features.

ISOPlot

A class for creating circumplex plots using different backends.

ExperimentalWarning

Bases: Warning

A warning class to signify experimental features.

ISOPlot

ISOPlot(
    data: DataFrame | None = None,
    x: str | ndarray | Series | None = "ISOPleasant",
    y: str | ndarray | Series | None = "ISOEventful",
    title: str | None = "Soundscape Density Plot",
    hue: str | None = None,
    palette: SeabornPaletteType | None = "colorblind",
    figure: Figure | None = None,
    axes: Axes | ndarray | None = None,
)

A class for creating circumplex plots using different backends.

This class provides methods for creating scatter plots and density plots based on the circumplex model of soundscape perception.

Examples:

>>> from soundscapy import isd, surveys
>>> df = isd.load()
>>> df = surveys.add_iso_coords(df)
>>> ct = isd.select_location_ids(df, ["CamdenTown", "RegentsParkJapan"])
>>> cp = (ISOPlot(ct, hue="LocationID")
...         .create_subplots()
...         .add_scatter()
...         .add_density()
...         .style())
>>> cp.show()

Initialize a ISOPlot instance.

PARAMETER DESCRIPTION
data

The data to be plotted, by default None

TYPE: DataFrame | None DEFAULT: None

x

Column name or data for x-axis, by default "ISOPleasant"

TYPE: str | ndarray | Series | None DEFAULT: 'ISOPleasant'

y

Column name or data for y-axis, by default "ISOEventful"

TYPE: str | ndarray | Series | None DEFAULT: 'ISOEventful'

title

Title of the plot, by default "Soundscape Density Plot"

TYPE: str | None DEFAULT: 'Soundscape Density Plot'

hue

Column name for color encoding, by default None

TYPE: str | None DEFAULT: None

palette

Color palette to use, by default "colorblind"

TYPE: SeabornPaletteType | None DEFAULT: 'colorblind'

figure

Existing figure to plot on, by default None

TYPE: Figure | None DEFAULT: None

axes

Existing axes to plot on, by default None

TYPE: Axes | ndarray | None DEFAULT: None

Examples:

Create a plot with default parameters:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...    columns=['ISOPleasant', 'ISOEventful']
... )
>>> plot = ISOPlot()
>>> isinstance(plot, ISOPlot)
True

Create a plot with a DataFrame:

>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = ISOPlot(data=data, hue='Group')
>>> plot.hue
'Group'

Create a plot directly with arrays:

>>> x, y = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], 100).T
>>> plot = ISOPlot(x=x, y=y)
>>> isinstance(plot, ISOPlot)
True
METHOD DESCRIPTION
create_subplots

Create subplots for the circumplex plot.

show

Show the figure.

close

Close the figure.

savefig

Save the figure.

get_figure

Get the figure object.

get_axes

Get the axes object.

get_single_axes

Get a specific axes object.

yield_axes_objects

Generate a sequence of axes objects to iterate over.

add_layer

Add a visualization layer, optionally targeting specific subplot(s).

add_scatter

Add a scatter layer to specific subplot(s).

add_spi

Add a SPI layer to specific subplot(s).

add_density

Add a density layer to specific subplot(s).

add_simple_density

Add a simple density layer to specific subplot(s).

add_annotation

Add an annotation to the plot.

style

Apply styling to the plot.

ATTRIBUTE DESCRIPTION
x

Get the x-axis column name.

TYPE: str

y

Get the y-axis column name.

TYPE: str

hue

Get the hue column name.

TYPE: str | None

title

Get the plot title.

TYPE: str | None

Source code in src/soundscapy/plotting/iso_plot.py
def __init__(
    self,
    data: pd.DataFrame | None = None,
    x: str | np.ndarray | pd.Series | None = "ISOPleasant",
    y: str | np.ndarray | pd.Series | None = "ISOEventful",
    title: str | None = "Soundscape Density Plot",
    hue: str | None = None,
    palette: SeabornPaletteType | None = "colorblind",
    figure: Figure | None = None,  # Removed SubFigure type, don't think we need it
    axes: Axes | np.ndarray | None = None,
) -> None:
    """
    Initialize a ISOPlot instance.

    Parameters
    ----------
    data
        The data to be plotted, by default None
    x
        Column name or data for x-axis, by default "ISOPleasant"
    y
        Column name or data for y-axis, by default "ISOEventful"
    title
        Title of the plot, by default "Soundscape Density Plot"
    hue
        Column name for color encoding, by default None
    palette
        Color palette to use, by default "colorblind"
    figure
        Existing figure to plot on, by default None
    axes
        Existing axes to plot on, by default None

    Examples
    --------
    Create a plot with default parameters:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...    columns=['ISOPleasant', 'ISOEventful']
    ... )
    >>> plot = ISOPlot()
    >>> isinstance(plot, ISOPlot)
    True

    Create a plot with a DataFrame:
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = ISOPlot(data=data, hue='Group')
    >>> plot.hue
    'Group'


    Create a plot directly with arrays:

    >>> x, y = rng.multivariate_normal([0, 0], [[1, 0], [0, 1]], 100).T
    >>> plot = ISOPlot(x=x, y=y)
    >>> isinstance(plot, ISOPlot)
    True

    """
    warnings.warn(
        "`ISOPlot` is currently under development and should be considered "
        "experimental. `ISOPlot` implements an experimental API for creating "
        "layered soundscape circumplex plots. Use with caution.",
        ExperimentalWarning,
        stacklevel=2,
    )

    # Process and validate input data and coordinates
    data, x, y = self._check_data_x_y(data, x, y)
    self._check_data_hue(data, hue)

    # Initialize the main plot context
    self.main_context = PlotContext(
        data=data,
        x=x if isinstance(x, str) else DEFAULT_XCOL,
        y=y if isinstance(y, str) else DEFAULT_YCOL,
        hue=hue,
        title=title,
    )

    # Store additional plot attributes
    self.figure = figure
    self.axes = axes
    self.palette = palette

    # Initialize subplot management
    self.subplot_contexts: list[PlotContext] = []
    self.subplots_params = SubplotsParams()

    # Initialize parameter managers
    self._scatter_params = ScatterParams(
        data=data,
        x=self.main_context.x,
        y=self.main_context.y,
        hue=hue,
        palette=self.palette,
    )

    self._density_params = DensityParams(
        data=data,
        x=self.main_context.x,
        y=self.main_context.y,
        hue=hue,
        palette=self.palette,
    )

    self._simple_density_params = SimpleDensityParams(
        data=data,
        x=self.main_context.x,
        y=self.main_context.y,
        hue=hue,
    )

    self._spi_scatter_params = NotImplementedError
    self._spi_density_params = NotImplementedError
    self._spi_simple_density_params = SPISimpleDensityParams(
        x=self.main_context.x,
        y=self.main_context.y,
    )

    self._style_params = StyleParams()

    # SPI-related attributes
    self._spi_data = None

x property

x: str

Get the x-axis column name.

y property

y: str

Get the y-axis column name.

hue property

hue: str | None

Get the hue column name.

title property

title: str | None

Get the plot title.

create_subplots

create_subplots(
    nrows: int = 1,
    ncols: int = 1,
    figsize: tuple[int, int] = (5, 5),
    subplot_by: str | None = None,
    subplot_datas: list[DataFrame] | None = None,
    subplot_titles: list[str] | None = None,
    *,
    adjust_figsize: bool = True,
    auto_allocate_axes: bool = False,
    **kwargs,
) -> ISOPlot

Create subplots for the circumplex plot.

PARAMETER DESCRIPTION
nrows

Number of rows in the subplot grid, by default 1

TYPE: int DEFAULT: 1

ncols

Number of columns in the subplot grid, by default 1

TYPE: int DEFAULT: 1

figsize

Size of the figure (width, height), by default (5, 5)

TYPE: tuple[int, int] DEFAULT: (5, 5)

subplot_by

Column name to create subplots by unique values, by default None

TYPE: str | None DEFAULT: None

subplot_datas

List of dataframes for each subplot, by default None

TYPE: list[DataFrame] | None DEFAULT: None

subplot_titles

List of titles for each subplot, by default None

TYPE: list[str] | None DEFAULT: None

adjust_figsize

Whether to adjust the figure size based on nrows/ncols, by default True

TYPE: bool DEFAULT: True

auto_allocate_axes

Whether to automatically determine nrows/ncols based on data, by default False

TYPE: bool DEFAULT: False

**kwargs

Additional parameters for plt.subplots

DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Create a basic subplot grid:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = ISOPlot(data=data).create_subplots(nrows=2, ncols=2)
>>> len(plot.subplot_contexts) == 4
True
>>> plot.close()  # Clean up

Create subplots by a column in the data:

>>> plot = (ISOPlot(data=data)
...         .create_subplots(nrows=1, ncols=2, subplot_by='Group'))
>>> len(plot.subplot_contexts) == 2
True
>>> plot.close()  # Clean up

Create subplots with auto-allocation of axes:

>>> plot = (ISOPlot(data=data)
...        .create_subplots(subplot_by='Group', auto_allocate_axes=True))
>>> len(plot.subplot_contexts) == 2
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
def create_subplots(
    self,
    nrows: int = 1,
    ncols: int = 1,
    figsize: tuple[int, int] = (5, 5),
    subplot_by: str | None = None,
    subplot_datas: list[pd.DataFrame] | None = None,
    subplot_titles: list[str] | None = None,
    *,
    adjust_figsize: bool = True,
    auto_allocate_axes: bool = False,
    **kwargs,
) -> ISOPlot:
    """
    Create subplots for the circumplex plot.

    Parameters
    ----------
    nrows
        Number of rows in the subplot grid, by default 1
    ncols
        Number of columns in the subplot grid, by default 1
    figsize
        Size of the figure (width, height), by default (5, 5)
    subplot_by
        Column name to create subplots by unique values, by default None
    subplot_datas
        List of dataframes for each subplot, by default None
    subplot_titles
        List of titles for each subplot, by default None
    adjust_figsize
        Whether to adjust the figure size based on nrows/ncols, by default True
    auto_allocate_axes
        Whether to automatically determine nrows/ncols based on data,
        by default False
    **kwargs :
        Additional parameters for plt.subplots

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Create a basic subplot grid:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = ISOPlot(data=data).create_subplots(nrows=2, ncols=2)
    >>> len(plot.subplot_contexts) == 4
    True
    >>> plot.close()  # Clean up

    Create subplots by a column in the data:

    >>> plot = (ISOPlot(data=data)
    ...         .create_subplots(nrows=1, ncols=2, subplot_by='Group'))
    >>> len(plot.subplot_contexts) == 2
    True
    >>> plot.close()  # Clean up

    Create subplots with auto-allocation of axes:

    >>> plot = (ISOPlot(data=data)
    ...        .create_subplots(subplot_by='Group', auto_allocate_axes=True))
    >>> len(plot.subplot_contexts) == 2
    True
    >>> plot.close()  # Clean up

    """
    # Set up subplot params
    self.subplots_params.update(
        nrows=nrows,
        ncols=ncols,
        figsize=figsize,
        subplot_by=subplot_by,
        adjust_figsize=adjust_figsize,
        auto_allocate_axes=auto_allocate_axes,
        **kwargs,
    )
    # Create a list of dataframes and titles for each subplot
    # based on the unique values in the specified column
    if self.subplots_params.subplot_by:
        logger.debug(
            "Creating subplots by unique values "
            f"in {self.subplots_params.subplot_by}."
        )
        subplot_datas, subplot_titles, n_subplots_by = self._setup_subplot_by(
            self.subplots_params.subplot_by, subplot_datas, subplot_titles
        )
    else:
        n_subplots_by = -1

    if subplot_titles and self.subplots_params.auto_allocate_axes:
        # Attempt to allocate axes based on the number of subplots
        self.subplots_params.nrows, self.subplots_params.ncols = (
            self._allocate_subplot_axes(subplot_titles)
        )

    if adjust_figsize:
        self.subplots_params.figsize = (
            self.subplots_params.ncols * self.subplots_params.figsize[0],
            self.subplots_params.nrows * self.subplots_params.figsize[1],
        )

    logger.debug(f"Subplot parameters: {self.subplots_params}")

    # Create the figure and axes
    self.figure, self.axes = plt.subplots(
        **self.subplots_params.as_plt_subplots_args()
    )

    # If subplot_datas or subplot_titles are provided, validate them
    if subplot_datas is not None or subplot_titles is not None:
        self._validate_subplots_datas(subplot_datas, subplot_titles)

    # Create PlotContext objects for each subplot
    self.subplot_contexts = []

    for i, ax in enumerate(self.yield_axes_objects()):
        if i >= self._naxes:
            break
        if subplot_by and i >= n_subplots_by:
            logger.debug(f"Created {i + 1} subplots for {subplot_by}.")
            break
        # Get data and title for this subplot if available
        data = (
            subplot_datas[i] if subplot_datas and i < len(subplot_datas) else None
        )
        title = (
            subplot_titles[i]
            if subplot_titles and i < len(subplot_titles)
            else None
        )

        context = self.main_context.create_child(data=data, title=title, ax=ax)
        self.subplot_contexts.append(context)

    return self

show

show() -> None

Show the figure.

This method is a wrapper around plt.show() to display the figure.

Source code in src/soundscapy/plotting/iso_plot.py
def show(self) -> None:
    """
    Show the figure.

    This method is a wrapper around plt.show() to display the figure.

    """
    if self.figure is None:
        msg = (
            "No figure object provided. "
            "Please create a figure using create_subplots() first."
        )
        raise ValueError(msg)
    if self._has_subplots:
        plt.tight_layout()
    plt.show()

close

close(fig: int | str | Figure | None = None) -> None

Close the figure.

This method is a wrapper around plt.close() to close the figure.

Source code in src/soundscapy/plotting/iso_plot.py
def close(self, fig: int | str | Figure | None = None) -> None:
    """
    Close the figure.

    This method is a wrapper around plt.close() to close the figure.

    """
    if fig is None:
        fig = self.figure
        if fig is None:
            msg = (
                "No figure object provided. "
                "Please create a figure using create_subplots() first."
            )
            raise ValueError(msg)
    plt.close(fig)

savefig

savefig(*args: Any, **kwargs: Any) -> None

Save the figure.

This method is a wrapper around plt.savefig() to save the figure.

Source code in src/soundscapy/plotting/iso_plot.py
def savefig(self, *args: Any, **kwargs: Any) -> None:
    """
    Save the figure.

    This method is a wrapper around plt.savefig() to save the figure.

    """
    if self.figure is None:
        msg = (
            "No figure object provided. "
            "Please create a figure using create_subplots() first."
        )
        raise ValueError(msg)
    self.figure.savefig(*args, **kwargs)

get_figure

get_figure() -> Figure | SubFigure

Get the figure object.

RETURNS DESCRIPTION
Figure | SubFigure

The matplotlib Figure or SubFigure object associated with this plot.

RAISES DESCRIPTION
ValueError

If the figure object does not exist.

TypeError

If the figure object is not a valid Figure or SubFigure.

Source code in src/soundscapy/plotting/iso_plot.py
def get_figure(self) -> Figure | SubFigure:
    """
    Get the figure object.

    Returns
    -------
    :
        The matplotlib Figure or SubFigure object associated with this plot.


    Raises
    ------
    ValueError
        If the figure object does not exist.
    TypeError
        If the figure object is not a valid Figure or SubFigure.

    """
    if self.figure is None:
        msg = (
            "No figure object provided. "
            "Please create a figure using create_subplots() first."
        )
        raise ValueError(msg)
    if isinstance(self.figure, Figure | SubFigure):
        return self.figure
    msg = "Invalid figure object. Please provide a valid Figure or SubFigure."
    raise TypeError(msg)

get_axes

get_axes() -> Axes | np.ndarray

Get the axes object.

RETURNS DESCRIPTION
Axes | ndarray

The matplotlib Axes object or array of Axes associated with this plot.

RAISES DESCRIPTION
ValueError

If the axes object does not exist.

TypeError

If the axes object is not a valid Axes or ndarray of Axes.

Source code in src/soundscapy/plotting/iso_plot.py
def get_axes(self) -> Axes | np.ndarray:
    """
    Get the axes object.

    Returns
    -------
    :
        The matplotlib Axes object or array of Axes associated with this plot.

    Raises
    ------
    ValueError
        If the axes object does not exist.
    TypeError
        If the axes object is not a valid Axes or ndarray of Axes.

    """
    self._check_for_axes()
    if isinstance(self.axes, Axes | np.ndarray):
        return self.axes
    msg = "Invalid axes object. Please provide a valid Axes or ndarray of Axes."
    raise TypeError(msg)

get_single_axes

get_single_axes(
    ax_idx: int | tuple[int, int] | None = None,
) -> Axes

Get a specific axes object.

PARAMETER DESCRIPTION
ax_idx

The index of the axes to get. If None, returns the first axes. Can be an integer for flattened access or a tuple of (row, col).

TYPE: int | tuple[int, int] | None DEFAULT: None

RETURNS DESCRIPTION
Axes

The requested matplotlib Axes object

RAISES DESCRIPTION
ValueError

If the axes object does not exist or the index is invalid.

TypeError

If the axes object is not a valid Axes or ndarray of Axes.

Source code in src/soundscapy/plotting/iso_plot.py
def get_single_axes(self, ax_idx: int | tuple[int, int] | None = None) -> Axes:
    """
    Get a specific axes object.

    Parameters
    ----------
    ax_idx
        The index of the axes to get. If None, returns the first axes.
        Can be an integer for flattened access or a tuple of (row, col).

    Returns
    -------
    :
        The requested matplotlib Axes object

    Raises
    ------
    ValueError
        If the axes object does not exist or the index is invalid.
    TypeError
        If the axes object is not a valid Axes or ndarray of Axes.

    """
    self._check_for_axes()

    def validate_tuple_axes_index(
        nrows: int, ncols: int, naxes: int, ax_idx: tuple[int, int]
    ) -> None:
        """
        Validate the tuple axes index.

        This checks the `ax_idx` types and compares the implied number of axes
        with the actual number of axes in the figure.
        """
        if (
            len(ax_idx) != 2  # noqa: PLR2004
            or not isinstance(ax_idx[0], int)
            or not isinstance(ax_idx[1], int)
            or ax_idx[0] < 0
            or ax_idx[1] < 0
        ):
            msg = (
                "Invalid axes index provided. "
                "Expected a tuple of 2 positive integers."
            )
            raise ValueError(msg)

        if ax_idx[0] >= (nrows - 1) or ax_idx[1] >= (ncols - 1):
            msg = (
                "Invalid axes index provided."
                f" The figure contains {nrows} rows and {ncols} columns. "
                f"ax_idx implied {ax_idx[0] + 1} rows and {ax_idx[1] + 1} columns."
            )
            raise ValueError(msg)

        idx_implied_n_axes = (ax_idx[0] + 1) * (ax_idx[1] + 1)
        if naxes < idx_implied_n_axes:
            msg = (
                "Invalid axes index provided."
                f" The figure contains {naxes} axes. "
                f"ax_idx implied {idx_implied_n_axes} axes."
            )
            raise ValueError(msg)

    def validate_int_axes_index(naxes: int, ax_idx: int) -> None:
        """
        Validate the integer axes index.

        This checks the `ax_idx` type and compares the implied number of axes
        with the actual number of axes in the figure.
        """
        if not isinstance(ax_idx, int) or ax_idx < 0:
            msg = "Invalid axes index provided. Expected a positive integer."
            raise ValueError(msg)

        if (ax_idx + 1) > naxes:
            msg = (
                "Invalid axes index provided."
                f" The figure contains {naxes} axes. "
                f"ax_idx implied {ax_idx + 1} axes."
            )
            raise ValueError(msg)

    if isinstance(self.axes, np.ndarray) and ax_idx is not None:
        if isinstance(ax_idx, tuple):
            validate_tuple_axes_index(self._nrows, self._ncols, self._naxes, ax_idx)
            return self.axes[ax_idx[0], ax_idx[1]]

        validate_int_axes_index(self._naxes, ax_idx)
        return self.axes.flatten()[ax_idx]

    if isinstance(self.axes, Axes) and (ax_idx == 0 or ax_idx is None):
        return self.axes

    msg = "Invalid axes index provided."
    raise ValueError(msg)

yield_axes_objects

yield_axes_objects() -> Generator[Axes, None, None]

Generate a sequence of axes objects to iterate over.

This method is a helper to iterate over all axes in the figure, whether the figure contains a single Axes object or an array of Axes objects.

YIELDS DESCRIPTION
Axes

Individual matplotlib Axes objects from the current figure.

Source code in src/soundscapy/plotting/iso_plot.py
def yield_axes_objects(self) -> Generator[Axes, None, None]:
    """
    Generate a sequence of axes objects to iterate over.

    This method is a helper to iterate over all axes in the figure,
    whether the figure contains a single Axes object or an array of Axes objects.

    Yields
    ------
    :
        Individual matplotlib Axes objects from the current figure.

    """
    if isinstance(self.axes, np.ndarray):
        yield from self.axes.flatten()
    elif isinstance(self.axes, Axes):
        yield self.axes

add_layer

add_layer(
    layer_class: type[Layer],
    data: DataFrame | None = None,
    *,
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    **params: Any,
) -> ISOPlot

Add a visualization layer, optionally targeting specific subplot(s).

PARAMETER DESCRIPTION
layer_class

The type of layer to add

TYPE: type[Layer]

on_axis

Target specific axis/axes:

  • int: Index of subplot (flattened)
  • tuple: (row, col) coordinates
  • list: Multiple indices to apply the layer to
  • None: Apply to all subplots (default)

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific layer, overriding context data

TYPE: DataFrame | None DEFAULT: None

**params

Parameters for the layer

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a scatter layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> from soundscapy.plotting.layers import ScatterLayer
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> # Will create 2x2 subplots all with the same data
>>> plot = (ISOPlot(data=data)
...         .create_subplots(nrows=2, ncols=2)
...         .add_layer(ScatterLayer)
...         .style())
>>> plot.show()
>>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
    True
>>> plot.close()  # Clean up

Add a layer to a specific subplot:

>>> plot = (ISOPlot(data=data)
...         .create_subplots(nrows=2, ncols=2)
...         .add_layer(ScatterLayer, on_axis=0)
...         .style())
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> all(len(ctx.layers) == 0 for ctx in plot.subplot_contexts[1:])
True
>>> plot.close()

Add a layer to multiple subplots:

>>> plot = (ISOPlot(data=data)
...            .create_subplots(nrows=2, ncols=2)
...            .add_layer(ScatterLayer, on_axis=[0, 2])
...            .style())
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> len(plot.subplot_contexts[2].layers) == 1
True
>>> len(plot.subplot_contexts[1].layers) == 0
True
>>> plot.close()

Add a layer with custom data to a specific subplot:

>>> custom_data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
...     'ISOEventful': rng.normal(0.15, 0.2, 50),
... })
>>> plot = (ISOPlot(data=data)
...        .create_subplots(nrows=2, ncols=2)
...        .add_layer(ScatterLayer) # Add to all subplots
...        # Add a layer with custom data to the first subplot
...        .add_layer(ScatterLayer, data=data.iloc[:50], on_axis=0, color='red')
...        # Add a layer with custom data to the second subplot
...        .add_layer(ScatterLayer, data=custom_data, on_axis=1)
...        .style())
>>> plot.show()
>>> plot.close()
Source code in src/soundscapy/plotting/iso_plot.py
def add_layer(
    self,
    layer_class: type[Layer],
    data: pd.DataFrame | None = None,
    *,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    **params: Any,
) -> ISOPlot:
    """
    Add a visualization layer, optionally targeting specific subplot(s).

    Parameters
    ----------
    layer_class
        The type of layer to add
    on_axis
        Target specific axis/axes:

        - int: Index of subplot (flattened)
        - tuple: (row, col) coordinates
        - list: Multiple indices to apply the layer to
        - None: Apply to all subplots (default)
    data
        Custom data for this specific layer, overriding context data
    **params
        Parameters for the layer

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Add a scatter layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> from soundscapy.plotting.layers import ScatterLayer
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> # Will create 2x2 subplots all with the same data
    >>> plot = (ISOPlot(data=data)
    ...         .create_subplots(nrows=2, ncols=2)
    ...         .add_layer(ScatterLayer)
    ...         .style())
    >>> plot.show() # doctest: +SKIP
    >>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
        True
    >>> plot.close()  # Clean up

    Add a layer to a specific subplot:

    >>> plot = (ISOPlot(data=data)
    ...         .create_subplots(nrows=2, ncols=2)
    ...         .add_layer(ScatterLayer, on_axis=0)
    ...         .style())
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> all(len(ctx.layers) == 0 for ctx in plot.subplot_contexts[1:])
    True
    >>> plot.close()

    Add a layer to multiple subplots:

    >>> plot = (ISOPlot(data=data)
    ...            .create_subplots(nrows=2, ncols=2)
    ...            .add_layer(ScatterLayer, on_axis=[0, 2])
    ...            .style())
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> len(plot.subplot_contexts[2].layers) == 1
    True
    >>> len(plot.subplot_contexts[1].layers) == 0
    True
    >>> plot.close()

    Add a layer with custom data to a specific subplot:
    >>> custom_data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
    ...     'ISOEventful': rng.normal(0.15, 0.2, 50),
    ... })
    >>> plot = (ISOPlot(data=data)
    ...        .create_subplots(nrows=2, ncols=2)
    ...        .add_layer(ScatterLayer) # Add to all subplots
    ...        # Add a layer with custom data to the first subplot
    ...        .add_layer(ScatterLayer, data=data.iloc[:50], on_axis=0, color='red')
    ...        # Add a layer with custom data to the second subplot
    ...        .add_layer(ScatterLayer, data=custom_data, on_axis=1)
    ...        .style())
    >>> plot.show() # doctest: +SKIP
    >>> plot.close()

    """
    # TODO(MitchellAcoustics): Need to handle legend/label creation
    #                          for new data added to a specific subplot
    # Create the layer instance
    layer = layer_class(custom_data=data, **params)

    # Check if we have axes to render on
    self._check_for_axes()

    # If no subplots created yet, add to main context
    if not self.subplot_contexts:
        if self.main_context.ax is None:
            # Get the single axis and assign it to main context
            if isinstance(self.axes, Axes):
                self.main_context.ax = self.axes
            elif isinstance(self.axes, np.ndarray) and self.axes.size > 0:
                self.main_context.ax = self.axes.flatten()[0]

        # Add layer to main context
        self.main_context.layers.append(layer)
        # Render the layer immediately
        layer.render(self.main_context)
        return self

    # Handle various axis targeting options
    target_contexts = self._resolve_target_contexts(on_axis)
    logger.debug(f"N target contexts: {len(target_contexts)}")

    # Add the layer to each target context and render it
    for i, context in enumerate(target_contexts):
        if data is not None and i >= self.subplots_params.n_subplots_by > 0:
            # If custom data is provided, use it for the specific subplot
            break
        context.layers.append(layer)
        layer.render(context)

    return self

add_scatter

add_scatter(
    data: DataFrame | None = None,
    *,
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    **params: Any,
) -> ISOPlot

Add a scatter layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific scatter plot

TYPE: DataFrame | None DEFAULT: None

**params

Parameters for the scatter plot

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a scatter layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = (ISOPlot(data=data)
...           .create_subplots(nrows=2, ncols=1)
...           .add_scatter(s=50, alpha=0.7, hue='Group')
...           .style())
>>> plot.show()
>>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
True
>>> plot.close()  # Clean up

Add a scatter layer with custom data to a specific subplot:

>>> custom_data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
...     'ISOEventful': rng.normal(0.15, 0.2, 50),
... })
>>> plot = (ISOPlot(data=data)
...            .create_subplots(nrows=2, ncols=1)
...            .add_scatter(hue='Group')
...            .add_scatter(on_axis=0, data=custom_data, color='red')
...            .style())
>>> plot.show()
>>> plot.subplot_contexts[0].layers[1].custom_data is custom_data
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
def add_scatter(
    self,
    data: pd.DataFrame | None = None,
    *,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    **params: Any,
) -> ISOPlot:
    """
    Add a scatter layer to specific subplot(s).

    Parameters
    ----------
    on_axis
        Target specific axis/axes
    data
        Custom data for this specific scatter plot
    **params
        Parameters for the scatter plot

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Add a scatter layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = (ISOPlot(data=data)
    ...           .create_subplots(nrows=2, ncols=1)
    ...           .add_scatter(s=50, alpha=0.7, hue='Group')
    ...           .style())
    >>> plot.show() # doctest: +SKIP
    >>> all(len(ctx.layers) == 1 for ctx in plot.subplot_contexts)
    True
    >>> plot.close()  # Clean up

    Add a scatter layer with custom data to a specific subplot:

    >>> custom_data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.1, 50),
    ...     'ISOEventful': rng.normal(0.15, 0.2, 50),
    ... })
    >>> plot = (ISOPlot(data=data)
    ...            .create_subplots(nrows=2, ncols=1)
    ...            .add_scatter(hue='Group')
    ...            .add_scatter(on_axis=0, data=custom_data, color='red')
    ...            .style())
    >>> plot.show() # doctest: +SKIP
    >>> plot.subplot_contexts[0].layers[1].custom_data is custom_data
    True
    >>> plot.close()  # Clean up

    """
    # Merge default scatter parameters with provided ones
    # Remove data from scatter_params to avoid conflict
    scatter_params = self._scatter_params.copy()
    scatter_params.drop("data")
    scatter_params.update(**params)

    return self.add_layer(
        ScatterLayer,
        data=data,
        on_axis=on_axis,
        **scatter_params.as_dict(drop=["data"]),
    )

add_spi

add_spi(
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    spi_target_data: DataFrame | ndarray | None = None,
    msn_params: DirectParams | CentredParams | None = None,
    *,
    layer_class: type[Layer] = SPISimpleLayer,
    **params: Any,
) -> ISOPlot

Add a SPI layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

spi_target_data

Custom data for this specific SPI plot

TYPE: DataFrame | ndarray | None DEFAULT: None

msn_params

Parameters for the SPI plot

TYPE: DirectParams | CentredParams | None DEFAULT: None

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a SPI layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> from soundscapy.spi import DirectParams
>>> rng = np.random.default_rng(42)
>>>    # Create a DataFrame with random data
>>> data = pd.DataFrame(
...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...    columns=['ISOPleasant', 'ISOEventful']
... )
>>>    # Define MSN parameters for the SPI target
>>> msn_params = DirectParams(
...     xi=np.array([0.5, 0.7]),
...     omega=np.array([[0.1, 0.05], [0.05, 0.1]]),
...     alpha=np.array([0, -5]),
...     )
>>>    # Create the plot with only an SPI layer
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_scatter()
...     .add_spi(msn_params=msn_params)
...     .style()
... )
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()  # Clean up

Add an SPI layer over top of 'real' data:

>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_scatter()
...     .add_density()
...     .add_spi(msn_params=msn_params, show_score="on axis")
...     .style()
... )
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 3
True

Add a SPI layer from spi data:

>>> # Create a custom distribution
>>> from soundscapy.spi import MultiSkewNorm
>>> import soundscapy as sspy
>>> spi_msn = MultiSkewNorm.from_params(msn_params)
>>> # Generate random samples
>>> spi_msn.sample(1000)
>>> data = sspy.add_iso_coords(sspy.isd.load())
>>> data = sspy.isd.select_location_ids(
...     data,
...     ['CamdenTown', 'PancrasLock', 'RussellSq', 'RegentsParkJapan']
... )
>>> mp3 = (
...     ISOPlot(
...         data=data,
...         title="Soundscape Density Plots with corrected ISO coordinates",
...         hue="SessionID",
...     )
...     .create_subplots(
...         subplot_by="LocationID",
...         figsize=(4, 4),
...         auto_allocate_axes=True,
...     )
...     .add_scatter()
...     .add_simple_density(fill=False)
...     .add_spi(spi_target_data=spi_msn.sample_data, show_score="under title")
...     .style()
... )
>>> mp3.show()
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
def add_spi(
    self,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    spi_target_data: pd.DataFrame | np.ndarray | None = None,
    msn_params: DirectParams | CentredParams | None = None,
    *,
    layer_class: type[Layer] = SPISimpleLayer,
    **params: Any,
) -> ISOPlot:
    """
    Add a SPI layer to specific subplot(s).

    Parameters
    ----------
    on_axis
        Target specific axis/axes
    spi_target_data
        Custom data for this specific SPI plot
    msn_params
        Parameters for the SPI plot

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Add a SPI layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> from soundscapy.spi import DirectParams
    >>> rng = np.random.default_rng(42)
    >>>    # Create a DataFrame with random data
    >>> data = pd.DataFrame(
    ...    rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...    columns=['ISOPleasant', 'ISOEventful']
    ... )
    >>>    # Define MSN parameters for the SPI target
    >>> msn_params = DirectParams(
    ...     xi=np.array([0.5, 0.7]),
    ...     omega=np.array([[0.1, 0.05], [0.05, 0.1]]),
    ...     alpha=np.array([0, -5]),
    ...     )
    >>>    # Create the plot with only an SPI layer
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_spi(msn_params=msn_params)
    ...     .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 2
    True
    >>> plot.close()  # Clean up

    Add an SPI layer over top of 'real' data:
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_density()
    ...     .add_spi(msn_params=msn_params, show_score="on axis")
    ...     .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 3
    True

    Add a SPI layer from spi data:
    >>> # Create a custom distribution
    >>> from soundscapy.spi import MultiSkewNorm
    >>> import soundscapy as sspy
    >>> spi_msn = MultiSkewNorm.from_params(msn_params)
    >>> # Generate random samples
    >>> spi_msn.sample(1000)
    >>> data = sspy.add_iso_coords(sspy.isd.load())
    >>> data = sspy.isd.select_location_ids(
    ...     data,
    ...     ['CamdenTown', 'PancrasLock', 'RussellSq', 'RegentsParkJapan']
    ... )

    >>> mp3 = (
    ...     ISOPlot(
    ...         data=data,
    ...         title="Soundscape Density Plots with corrected ISO coordinates",
    ...         hue="SessionID",
    ...     )
    ...     .create_subplots(
    ...         subplot_by="LocationID",
    ...         figsize=(4, 4),
    ...         auto_allocate_axes=True,
    ...     )
    ...     .add_scatter()
    ...     .add_simple_density(fill=False)
    ...     .add_spi(spi_target_data=spi_msn.sample_data, show_score="under title")
    ...     .style()
    ... )
    >>> mp3.show() # doctest: +SKIP
    >>> plot.close()  # Clean up

    """
    # BUG: This last doctest doesn't show the spi score under the title

    if layer_class == SPISimpleLayer:
        spi_simple_params = self._spi_simple_density_params.copy()
        spi_simple_params.drop("data")
        spi_simple_params.update(**params)

        return self.add_layer(
            layer_class,
            on_axis=on_axis,
            msn_params=msn_params,
            spi_target_data=spi_target_data,
            **spi_simple_params.as_dict(drop=["data"]),
        )
    if layer_class in (SPIDensityLayer, SPIScatterLayer):
        msg = (
            "Only the simple density layer type is currently supported for "
            "SPI plots. Please use SPISimpleLayer"
        )
        raise NotImplementedError(msg)

    msg = "Invalid layer class provided. Expected SPISimpleLayer. "
    raise ValueError(msg)

add_density

add_density(
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    data: DataFrame | None = None,
    *,
    include_outline: bool = False,
    **params: Any,
) -> ISOPlot

Add a density layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific density plot

TYPE: DataFrame | None DEFAULT: None

include_outline

Whether to include an outline around the density plot, by default False

TYPE: bool DEFAULT: False

**params

Parameters for the density plot

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a density layer to all subplots:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.25, 50),
...     'ISOEventful': rng.normal(0.15, 0.4, 50),
... })
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_density()
...     .style()
... )
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> plot.close()  # Clean up

Add a density layer with custom settings:

>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_density(levels=5, alpha=0.7)
...     .style()
... )
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 1
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
def add_density(
    self,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    data: pd.DataFrame | None = None,
    *,
    include_outline: bool = False,
    **params: Any,
) -> ISOPlot:
    """
    Add a density layer to specific subplot(s).

    Parameters
    ----------
    on_axis
        Target specific axis/axes
    data
        Custom data for this specific density plot
    include_outline
        Whether to include an outline around the density plot, by default False
    **params
        Parameters for the density plot

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Add a density layer to all subplots:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.25, 50),
    ...     'ISOEventful': rng.normal(0.15, 0.4, 50),
    ... })
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_density()
    ...     .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> plot.close()  # Clean up

    Add a density layer with custom settings:

    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_density(levels=5, alpha=0.7)
    ...     .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 1
    True
    >>> plot.close()  # Clean up

    """
    # Merge default density parameters with provided ones
    density_params = self._density_params.copy()
    density_params.drop("data")
    density_params.update(**params)

    return self.add_layer(
        DensityLayer,
        data=data,
        on_axis=on_axis,
        include_outline=include_outline,
        **density_params.as_dict(drop=["data"]),
    )

add_simple_density

add_simple_density(
    on_axis: int
    | tuple[int, int]
    | list[int]
    | None = None,
    data: DataFrame | None = None,
    *,
    include_outline: bool = True,
    **params: Any,
) -> ISOPlot

Add a simple density layer to specific subplot(s).

PARAMETER DESCRIPTION
on_axis

Target specific axis/axes

TYPE: int | tuple[int, int] | list[int] | None DEFAULT: None

data

Custom data for this specific density plot

TYPE: DataFrame | None DEFAULT: None

include_outline

Whether to include an outline around the density plot, by default True

TYPE: bool DEFAULT: True

**params

Additional parameters for the density plot. Useful options include thresh (default 0.5), levels (default 2), and alpha (default 0.5).

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Add a simple density layer:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> data = pd.DataFrame({
...     'ISOPleasant': rng.normal(0.2, 0.25, 30),
...     'ISOEventful': rng.normal(0.15, 0.4, 30),
... })
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots()
...     .add_scatter()
...     .add_simple_density()
...     .style()
... )
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()  # Clean up

Add a simple density with splitting by group:

>>> data = pd.DataFrame(
...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...          rng.integers(1, 3, 100)],
...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> plot = (
...     ISOPlot(data=data, hue='Group')
...     .create_subplots()
...     .add_scatter()
...     .add_simple_density()
...     .style()
... )
>>> plot.show()
>>> len(plot.subplot_contexts[0].layers) == 2
True
>>> plot.close()
...
Source code in src/soundscapy/plotting/iso_plot.py
def add_simple_density(
    self,
    on_axis: int | tuple[int, int] | list[int] | None = None,
    data: pd.DataFrame | None = None,
    *,
    include_outline: bool = True,
    **params: Any,
) -> ISOPlot:
    """
    Add a simple density layer to specific subplot(s).

    Parameters
    ----------
    on_axis
        Target specific axis/axes
    data
        Custom data for this specific density plot
    include_outline
        Whether to include an outline around the density plot, by default True
    **params
        Additional parameters for the density plot. Useful options include
        `thresh` (default `0.5`), `levels` (default `2`), and `alpha`
        (default `0.5`).

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Add a simple density layer:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> data = pd.DataFrame({
    ...     'ISOPleasant': rng.normal(0.2, 0.25, 30),
    ...     'ISOEventful': rng.normal(0.15, 0.4, 30),
    ... })
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_simple_density()
    ...     .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 2
    True
    >>> plot.close()  # Clean up

    Add a simple density with splitting by group:
    >>> data = pd.DataFrame(
    ...    np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...          rng.integers(1, 3, 100)],
    ...    columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> plot = (
    ...     ISOPlot(data=data, hue='Group')
    ...     .create_subplots()
    ...     .add_scatter()
    ...     .add_simple_density()
    ...     .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> len(plot.subplot_contexts[0].layers) == 2
    True
    >>> plot.close()
    ...

    """
    # Merge default simple density parameters with provided ones
    simple_density_params = self._simple_density_params.copy()
    simple_density_params.drop("data")
    simple_density_params.update(**params)

    return self.add_layer(
        SimpleDensityLayer,
        on_axis=on_axis,
        data=data,
        include_outline=include_outline,
        **simple_density_params.as_dict(drop=["data"]),
    )

add_annotation

add_annotation(
    text: str,
    xy: tuple[float, float],
    xytext: tuple[float, float],
    arrowprops: dict[str, Any] | None = None,
) -> ISOPlot

Add an annotation to the plot.

PARAMETER DESCRIPTION
text

The text to display in the annotation.

TYPE: str

xy

The point to annotate.

TYPE: tuple[float, float]

xytext

The point at which to place the text.

TYPE: tuple[float, float]

arrowprops

Properties for the arrow connecting the annotation text to the point.

TYPE: dict[str, Any] | None DEFAULT: None

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Source code in src/soundscapy/plotting/iso_plot.py
def add_annotation(
    self,
    text: str,
    xy: tuple[float, float],
    xytext: tuple[float, float],
    arrowprops: dict[str, Any] | None = None,
) -> ISOPlot:
    """
    Add an annotation to the plot.

    Parameters
    ----------
    text
        The text to display in the annotation.
    xy
        The point to annotate.
    xytext
        The point at which to place the text.
    arrowprops
        Properties for the arrow connecting the annotation text to the point.

    Returns
    -------
    :
        The current plot instance for chaining

    """
    msg = "AnnotationLayer is not yet implemented. "
    raise NotImplementedError(msg)
    # TODO(MitchellAcoustics): Implement AnnotationLayer
    return self.add_layer(
        "AnnotationLayer",
        text=text,
        xy=xy,
        xytext=xytext,
        arrowprops=arrowprops,
    )

style

style(**kwargs: Any) -> ISOPlot

Apply styling to the plot.

PARAMETER DESCRIPTION
**kwargs

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
ISOPlot

The current plot instance for chaining

Examples:

Apply styling with default parameters:

>>> import pandas as pd
>>> import numpy as np
>>> rng = np.random.default_rng(42)
>>> # Create simple data for styling example
>>> data = pd.DataFrame(
...     np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
...             rng.integers(1, 3, 100)],
...     columns=['ISOPleasant', 'ISOEventful', 'Group'])
>>> # Create plot with default styling
>>> plot = (
...    ISOPlot(data=data)
...       .create_subplots()
...       .add_scatter()
...       .style()
... )
>>> plot.show()
>>> plot.get_figure() is not None
True
>>> plot.close()  # Clean up

Apply styling with custom parameters:

>>> plot = (
...         ISOPlot(data=data)
...         .create_subplots()
...         .add_scatter()
...         .style(xlim=(-2, 2), ylim=(-2, 2), primary_lines=False)
... )
>>> plot.show()
>>> plot.get_figure() is not None
True
>>> plot.close()  # Clean up

Demonstrate the fluent interface (method chaining):

>>> # Create plot with method chaining
>>> plot = (
...     ISOPlot(data=data)
...     .create_subplots(nrows=1, ncols=1)
...     .add_scatter(alpha=0.7)
...     .add_density(levels=5)
...     .style(title_fontsize=14)
... )
>>> plot.show()
>>> # Verify results
>>> isinstance(plot, ISOPlot)
True
>>> plot.close()  # Clean up
Source code in src/soundscapy/plotting/iso_plot.py
def style(
    self,
    **kwargs: Any,
) -> ISOPlot:
    """
    Apply styling to the plot.

    Parameters
    ----------
    **kwargs

    Returns
    -------
    :
        The current plot instance for chaining

    Examples
    --------
    Apply styling with default parameters:

    >>> import pandas as pd
    >>> import numpy as np
    >>> rng = np.random.default_rng(42)
    >>> # Create simple data for styling example
    >>> data = pd.DataFrame(
    ...     np.c_[rng.multivariate_normal([0.2, 0.15], [[0.1, 0], [0, 0.2]], 100),
    ...             rng.integers(1, 3, 100)],
    ...     columns=['ISOPleasant', 'ISOEventful', 'Group'])
    >>> # Create plot with default styling
    >>> plot = (
    ...    ISOPlot(data=data)
    ...       .create_subplots()
    ...       .add_scatter()
    ...       .style()
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> plot.get_figure() is not None
    True
    >>> plot.close()  # Clean up

    Apply styling with custom parameters:

    >>> plot = (
    ...         ISOPlot(data=data)
    ...         .create_subplots()
    ...         .add_scatter()
    ...         .style(xlim=(-2, 2), ylim=(-2, 2), primary_lines=False)
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> plot.get_figure() is not None
    True
    >>> plot.close()  # Clean up

    Demonstrate the fluent interface (method chaining):

    >>> # Create plot with method chaining
    >>> plot = (
    ...     ISOPlot(data=data)
    ...     .create_subplots(nrows=1, ncols=1)
    ...     .add_scatter(alpha=0.7)
    ...     .add_density(levels=5)
    ...     .style(title_fontsize=14)
    ... )
    >>> plot.show() # doctest: +SKIP
    >>> # Verify results
    >>> isinstance(plot, ISOPlot)
    True
    >>> plot.close()  # Clean up

    """
    self._style_params.update(**kwargs)
    self._check_for_axes()

    self._set_style()
    self._circumplex_grid()
    self._set_title()
    self._set_axes_titles()
    self._primary_labels()
    if self._style_params.get("primary_lines"):
        self._primary_lines()
    if self._style_params.get("diagonal_lines"):
        self._diagonal_lines_and_labels()

    if self._style_params.get("legend_loc") is not False:
        self._move_legend()

    return self