#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Multi-plot orchestration
========================
This module offers utilities for orchestrating multiple synchronized
plots and highlighting specific data regions. It supports creating
complex visualizations with shared axes and unified legends, integrating
with tools like `multiplot` and `highlight`.
"""
# mypy: disable-error-code="attr-defined"
# Standard library imports for type checking and future compatibility
from __future__ import annotations
# Third-party imports for plotting and numerical operations
import colorsys
import copy
from itertools import cycle
from typing import TYPE_CHECKING, cast
import matplotlib.axes as mplaxes
import matplotlib.cm as cm
import matplotlib.patheffects as mpe
import matplotlib.pyplot as plt
import numpy as np
# Core and utility imports for exception handling
from ..util.exceptions import ParameterError
# Type checking imports for development and static analysis
if TYPE_CHECKING:
from typing import Any, Callable, Literal, Sequence
import cycler
import matplotlib
import matplotlib.figure
import numpy.typing as npt
from matplotlib.artist import Artist
from matplotlib.typing import ColorType
from .._typing import ArrayLike
# Module imports for image and signal display functions
from .image import specshow
from .signal import wavebars, waveshow
def _squeeze_shape(shape: tuple[int, ...]) -> tuple[int, ...]:
"""Check if two shape arrays are equivalent after squeezing out singleton dimensions."""
return tuple(dim for dim in shape if dim > 1)
def _resolve_multiplot(
func: Literal["waveshow", "wavebars", "specshow"],
) -> tuple[Callable[..., Any], int, list[str]]:
"""Resolve multiplot function names.
Parameters
----------
func : str
The name of the display function to use for the multiplot.
Accepted values are 'waveshow', 'wavebars', and 'specshow'.
Returns
-------
function : callable
The display function corresponding to the given name.
dims : int
The number of data dimensions that each call to the display function expects.
badprops : list of str
A list of property names that are not supported by the display function and should
be removed from the style cycle when sharing properties.
"""
display_map: dict[str, tuple[Callable[..., Any], int, list[str]]] = {
"waveshow": (waveshow, 1, []),
"wavebars": (wavebars, 1, []),
"specshow": (specshow, 2, ["color"]),
}
try:
return display_map[func]
except KeyError as exc:
raise ParameterError(f"Invalid display '{func}' for multiplot") from exc
def _mp_get_layout(
data: tuple[np.ndarray, ...], dims: int, orient: Literal["h", "v"]
) -> tuple[tuple[int, ...], int, int, bool]:
"""Determine the layout of a multiplot grid based on the data shape and orientation.
Parameters
----------
data : tuple of ndarray
The input data for the multiplot. The shape of this data will determine the layout of the grid.
dims : int
The number of data dimensions that each call to the display function expects.
orient : str {'h', 'v'}
The orientation of the multiplot grid. Accepted values are 'h' for horizontal and 'v' for vertical.
Returns
-------
axshape : tuple of int
The shape of the grid of axes, determined by the shape of the input data and the
specified orientation.
nrows : int
The number of rows in the grid of axes.
ncols : int
The number of columns in the grid of axes.
multi_input : bool
If the input contains multiple separate arrays to plot,
this flag is True. Otherwise, False.
"""
if orient not in ("h", "v"):
raise ParameterError(f"Invalid value orient={orient}")
multi_plot = False
if len(data) == 1 and isinstance(data[0], np.ndarray) and data[0].ndim > dims:
data_stack = np.asarray(data[0])
axshape = data_stack.shape[:-dims]
elif len(data) >= 1:
multi_plot = True
axshape = (len(data),)
else:
raise ParameterError("multiplot requires at least one data array to plot")
if len(axshape) == 1:
if orient == "v":
nrows, ncols = axshape[0], 1
else:
nrows, ncols = 1, axshape[0]
elif len(axshape) == 2:
# Yes this is awkward, but it makes the type checker work.
# In a sane world it would just be nrows, ncols = axshape
nrows, ncols = axshape[0], axshape[-1]
else:
raise ParameterError(f"Invalid axes shape={axshape}")
return axshape, nrows, ncols, multi_plot
def _mp_setup_axes(
*,
axes: matplotlib.axes.Axes | np.ndarray | None,
fig: matplotlib.figure.FigureBase | None = None,
fig_kw: dict | None = None,
nrows: int,
ncols: int,
axshape: tuple[int, ...],
orient: Literal["h", "v"],
sharex: bool,
sharey: bool,
) -> tuple[matplotlib.figure.FigureBase, npt.NDArray[np.object_], tuple[int, ...]]:
"""Set up the figure and axes for a multiplot grid.
Parameters
----------
axes : matplotlib.axes.Axes, np.ndarray, or None
The axes to use for the multiplot. If None, a new figure and axes will be created.
If a single Axes object is provided, it will be used for all subplots.
If an array of Axes objects is provided, it must be compatible with the shape of the data.
fig : matplotlib.figure.FigureBase or None
The figure to use for the multiplot. If None, a new figure will be created if needed.
fig_kw : dict or None
Additional keyword arguments to pass to `plt.subplots` when creating a new figure.
nrows : int
The number of rows in the grid of axes.
ncols : int
The number of columns in the grid of axes.
axshape : tuple of int
The shape of the grid of axes, determined by the shape of the input data and the
specified orientation.
orient : str {'h', 'v'}
The orientation of the multiplot grid. Accepted values are 'h' for horizontal and 'v' for vertical.
sharex : bool
Whether to share the x-axis among subplots when creating a new figure.
sharey : bool
Whether to share the y-axis among subplots when creating a new figure.
Returns
-------
fig : matplotlib.figure.FigureBase
The figure object for the multiplot.
axes : np.ndarray
An array of Axes objects for the multiplot, with shape compatible with the input data.
output_shape : tuple of int
The shape of the output array of display objects, determined by the shape of the axes.
"""
output_shape = axshape
if axes is None:
if fig is None:
if fig_kw is None:
fig_kw = {}
fig, axes = plt.subplots(
nrows=nrows,
ncols=ncols,
sharex=sharex,
sharey=sharey,
squeeze=False,
**fig_kw,
)
else:
axes = fig.subplots(
nrows=nrows,
ncols=ncols,
sharex=sharex,
sharey=sharey,
squeeze=False,
)
elif isinstance(axes, np.ndarray):
output_shape = axes.shape
if axes.ndim == 1:
if orient == "v":
axes = axes[:, np.newaxis]
else:
axes = axes[np.newaxis, :]
else:
if not isinstance(axes, np.ndarray):
output_shape = tuple()
axes = np.atleast_2d(np.asarray(axes))
# Ensure that axes object is now encapsulated in numpy arrays
axes = np.asarray(axes, dtype=object)
# Populate fig with the figure from the axes object.
fig = axes.flat[0].get_figure()
if _squeeze_shape(axes.shape) != _squeeze_shape(axshape):
raise ParameterError(f"axes shape={axes.shape} is incompatible with data shape")
return fig, axes, output_shape
def _mp_setup_labels(labels: Sequence[str | None] | None, shape: tuple[int, ...]) -> npt.NDArray[np.object_]:
"""Set up the labels for a multiplot grid.
Parameters
----------
labels : sequence of str or None
The labels to apply to each subplot in the multiplot grid. If None, no labels
will be applied. If a sequence is provided, it must be compatible with the shape of the axes.
shape : tuple of int
The shape of the grid of axes, determined by the shape of the input data and the
specified orientation.
Returns
-------
np.ndarray
An array of labels for each subplot in the multiplot grid, with shape compatible with the
axes.
"""
if labels is None:
return np.full(shape, None, dtype=object)
return np.asarray(labels, dtype=object).reshape(shape)
def _mp_setup_prop_group(
share_properties: bool | Literal["row", "col"] | ArrayLike | None,
shape: tuple[int, ...],
) -> np.ndarray:
"""Set up the property groups for a multiplot grid.
This is used to determine how style properties (color, line style, etc.) are shared among
different subplots in the grid.
Parameters
----------
share_properties : bool, str, sequence, or None
The property sharing scheme for the multiplot grid. Accepted values are:
- `None` or `False`: no properties are shared, and each subplot is treated as a unique group.
- `True`: all subplots share the same properties and belong to a single group.
- 'row': subplots in the same row share properties and belong to the same group.
- 'col': subplots in the same column share properties and belong to the same group.
- sequence: a sequence of group identifiers for each subplot. The length of the
sequence must match the total number of subplots (i.e., the product of the shape
of the axes).
shape : tuple of int
The shape of the grid of axes, determined by the shape of the input data and the
specified orientation.
Returns
-------
np.ndarray
An array of group identifiers for each subplot in the multiplot grid, with shape compatible with the axes.
"""
if share_properties is None or share_properties is False:
return np.arange(np.prod(shape)).reshape(shape)
if share_properties is True:
return np.ones(shape, dtype=int)
if isinstance(share_properties, str) and share_properties == "row":
return np.asarray(np.indices(shape)[0])
if isinstance(share_properties, str) and share_properties == "col":
return np.asarray(np.indices(shape)[-1])
prop_group = np.asarray(share_properties)
if prop_group.size != np.prod(shape):
raise ParameterError(f"Shape mismatch between axes={shape} and share_properties={prop_group.shape}")
return prop_group.reshape(shape)
def _mp_setup_properties(
prop_group: np.ndarray, badprops: list[str], prop_cycle: cycler.Cycler | None
) -> npt.NDArray[np.object_]:
"""Set up the properties for each subplot in a multiplot grid based on the property groups.
Parameters
----------
prop_group : np.ndarray
An array of group identifiers for each subplot in the multiplot grid, with shape compatible with the axes.
badprops : list of str
A list of property names that are not supported by the display function
and should be removed from the style cycle when sharing properties.
prop_cycle : cycler.Cycler or None
The property cycle to use for assigning properties to the subplots. If None, the
default property cycle from `plt.rcParams["axes.prop_cycle"]` will be used.
Returns
-------
np.ndarray
An array of property dictionaries for each subplot in the multiplot grid, with shape compatible with the axes.
"""
properties = np.empty(prop_group.shape, dtype=object)
properties.fill(None)
if prop_cycle is None:
prop_cycle = plt.rcParams["axes.prop_cycle"]
style_cycle = cycle(prop_cycle)
style_map = {}
for idx in np.ndindex(prop_group.shape):
group = prop_group[idx]
if group not in style_map:
style = copy.deepcopy(next(style_cycle))
for prop in badprops:
style.pop(prop, None)
style_map[group] = style
properties[idx] = style_map[group]
return properties
[docs]
def multiplot(
func: Literal["waveshow", "wavebars", "specshow"],
*data: np.ndarray,
axes: matplotlib.axes.Axes | np.ndarray | None = None,
fig: matplotlib.figure.FigureBase | None = None,
orient: Literal["v", "h"] = "v",
share_properties: bool | Literal["row", "col"] | np.ndarray | None = None,
fig_kw: dict | None = None,
sharex: bool = True,
sharey: bool = True,
label_outer: bool = True,
labels: Sequence[str | None] | None = None,
titles: Sequence[str | None] | None = None,
prop_cycle: cycler.Cycler | None = None,
**kwargs: Any,
) -> npt.NDArray[np.object_]:
"""Visualize multiple related waveforms or spectrograms on an array of subplots.
Example use cases include:
- Displaying multiple waveforms from a multi-channel audio file.
- Displaying multiple spectrograms from a multi-channel audio file.
Parameters
----------
func : str
The name of the display function to use for the multiplot. Accepted values are 'waveshow',
'wavebars', and 'specshow'.
*data : one or more `np.ndarray`
The input data for the multiplot.
If one array is provided, it is interpreted as a multi-channel array, where the leading
dimensions correspond to different channels or signals to plot.
If multiple arrays are provided, each array is treated as a single channel or input
signal, and visualized on its own subplot.
axes : matplotlib.axes.Axes, np.ndarray, or None
The axes to use for the multiplot. If None, a new axes array will be created on `fig`.
If an array of Axes objects is provided, it must be compatible with the shape of the data.
If a single axes object is provided, it will be interpreted as a 1x1 array (i.e. a single subplot).
fig : matplotlib.figure.FigureBase or None
The figure to use for the multiplot. If None, a new figure will be created if needed.
If `axes` is provided, the figure will be inferred from `axes` and the `fig` parameter
will be ignored.
orient : str {'h', 'v'}
The orientation of the multiplot grid. Accepted values are 'h' for horizontal
and 'v' for vertical. This determines how the subplots are arranged when the
input data has a single non-singleton dimension (e.g., shape (n, k) with k > 1).
share_properties : bool, str, np.ndarray, or None
The property sharing scheme for the multiplot grid. Accepted values are:
- `None` or `False`: no properties are shared, and each subplot is treated as a unique group.
- `True`: all subplots share the same properties and belong to a single group.
- 'row': subplots in the same row share properties and belong to the same group
- 'col': subplots in the same column share properties and belong to the same group.
- np.ndarray: a custom array of group identifiers for each subplot. The shape of the
array must match the shape of the axes grid. Any two elements with the same value
are considered to be in the same group and will share properties.
fig_kw : dict or None
Additional keyword arguments to pass to `plt.subplots` when creating a new figure.
sharex : bool
Whether to share the x-axis among subplots when creating a new figure.
sharey : bool
Whether to share the y-axis among subplots when creating a new figure.
label_outer : bool
Whether to only show labels on the outer axes when using shared axes.
labels : sequence of str or None
The labels to apply to each subplot in the multiplot grid. If None, no labels
will be applied. If a sequence is provided, it must be compatible with the shape of the axes.
titles : sequence of str or None
The titles to apply to each subplot in the multiplot grid. If None, no titles
will be applied. If a sequence is provided, it must be compatible with the shape of the axes.
prop_cycle : cycler.Cycler or None
The property cycle to use for assigning properties to the subplots. If None, the
default property cycle from `plt.rcParams["axes.prop_cycle"]` will be used.
**kwargs
Additional keyword arguments to pass to the display function for each subplot.
Returns
-------
np.ndarray
An array of display objects returned by the display function for each subplot in the multiplot grid
The shape of this array will be compatible with the shape of the axes grid.
See Also
--------
waveshow
wavebars
specshow
legend_for_axes
Examples
--------
Display multiple synchronized signals stacked in an array. We'll let multiplot create
the figure and axes objects for us.
>>> import matplotlib.pyplot as plt
>>> y, sr = librosa.loadx('choice', duration=10)
>>> yh, yp = librosa.effects.hpss(y)
>>> librosa.display.multiplot('waveshow', y, yh, yp,
... labels=['Original', 'Harmonic', 'Percussive'],
... # The remaining parameters are passed through to waveshow
... sr=sr,
... invert=True)
>>> plt.gcf().legend()
>>> plt.show()
Multiplot can also accept preconstructed axes as input, provided that they
are compatible with the shape of the data. The below example does this
with a spectrogram display.
>>> y_stack = librosa.to_multi(y, yh, yp)
>>> stft = librosa.stft(y=y_stack)
>>> fig, ax = plt.subplots(nrows=3, sharex=True, sharey=True, figsize=(8, 8))
>>> img = librosa.display.multiplot('specshow', stft, axes=ax,
... titles=['Original', 'Harmonic', 'Percussive'],
... x_axis='time', y_axis='log', vscale='dBFS')
>>> librosa.display.colorbar_db(img[0], ax=ax, label='dBFS')
>>> plt.show()
"""
# Identify the display function and the expected data dimensions for each subplot
function, dims, badprops = _resolve_multiplot(func)
# Determine the layout of the multiplot grid based on the data shape and orientation
axshape, nrows, ncols, multi_input = _mp_get_layout(data, dims, orient)
# Set up the figure and axes for the multiplot grid
fig, axes, output_shape = _mp_setup_axes(
axes=axes,
fig=fig,
fig_kw=fig_kw,
nrows=nrows,
ncols=ncols,
axshape=axshape,
orient=orient,
sharex=sharex,
sharey=sharey,
)
# Set up the labels and properties for each subplot in the multiplot grid
labels = _mp_setup_labels(labels, axes.shape)
titles = _mp_setup_labels(titles, axes.shape)
prop_group = _mp_setup_prop_group(share_properties, axes.shape)
properties: np.ndarray = _mp_setup_properties(prop_group, badprops, prop_cycle)
# Allocate the output array
output = np.empty_like(axes, dtype=object)
# Iterate over each subplot and call the display function with the appropriate data, axes, labels, and properties
for idx in np.ndindex(axshape):
flat_idx = np.ravel_multi_index(idx, axshape)
if multi_input:
# User provided variadic inputs, so use flat indexing
datum = data[flat_idx]
else:
# User already stacked the inputs into one array.
datum = data[0][idx]
output.flat[flat_idx] = function(
datum,
ax=axes.flat[flat_idx],
label=labels.flat[flat_idx],
**properties.flat[flat_idx],
**kwargs,
)
if titles.flat[flat_idx] is not None:
axes.flat[flat_idx].set_title(titles.flat[flat_idx])
if label_outer:
axes.flat[flat_idx].label_outer()
# Reshape the output array to match the shape of the axes grid
return output.reshape(output_shape)
[docs]
def legend_for_axes(
axes: matplotlib.axes.Axes | np.ndarray | list[matplotlib.axes.Axes] | None = None,
*,
fig: matplotlib.figure.Figure | None = None,
**kwargs: Any,
) -> matplotlib.legend.Legend:
"""Create a figure-level legend for a collection of axes.
This is similar to `matplotlib.figure.Figure.legend`, but it limits
the handle collection to only those belonging to the specified axes.
This makes it easier to create different legends for subsets of a subplot array.
Parameters
----------
axes : matplotlib.axes.Axes or array-like of Axes, optional
Axes to include in the legend aggregation.
If not provided, axes are taken from `fig.axes`, or from the
current figure if `fig` is not provided.
fig : matplotlib.figure.Figure, optional
Figure on which to create the legend.
If not provided, it is inferred from `axes`, or from `plt.gcf()`
if `axes` is also not provided.
**kwargs
Additional keyword arguments passed to `matplotlib.figure.Figure.legend`.
Returns
-------
legend : matplotlib.legend.Legend
The created legend.
Examples
--------
If no axes are provided, we aggregate legends across all subplots on the current figure:
>>> import matplotlib.pyplot as plt
>>> x = np.linspace(-10, 10, 100)
>>> fig, ax = plt.subplots(nrows=2, ncols=1, sharex=True)
>>> ax[0].plot(x, label='Line', color='C0')
>>> ax[1].plot(x**2, label='Parabola', color='C1')
>>> librosa.display.legend_for_axes()
>>> plt.show()
You can also specify a subset of axes to aggregate, and control the legend placement:
>>> fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True)
>>> ax[0, 0].plot(x, label='Line', color='C0')
>>> ax[0, 1].plot(x**2, label='Parabola', color='C1')
>>> ax[1, 0].plot(x**3, label='Cubic', color='C2')
>>> ax[1, 1].plot(x**4, label='Quartic', color='C3')
>>> librosa.display.legend_for_axes(axes=ax[0], loc='outside upper center')
>>> librosa.display.legend_for_axes(axes=ax[1], loc='outside lower center')
>>> plt.show()
"""
if axes is None:
if fig is None:
fig = plt.gcf()
axes = fig.axes
axes_array = np.atleast_1d(np.asarray(axes, dtype=object))
if len(axes_array.flat) == 0:
raise ParameterError("No axes provided for legend aggregation")
if fig is None:
fig = axes_array.flat[0].figure
for ax in axes_array.flat:
if ax.figure is not fig:
raise ParameterError("All axes must belong to the same figure")
handles: list[Artist] = []
labels: list[str] = []
for ax in axes_array.flat:
hlist, llist = ax.get_legend_handles_labels()
handles.extend(hlist)
labels.extend(llist)
return fig.legend(handles, labels, **kwargs)
def _get_ax_bright_highlight(
ax: mplaxes.Axes,
luminance_threshold: float = 0.5,
) -> bool:
"""Determine whether the axes should produce a bright or dark
highlight.
This is based on a few things:
- If the axes has mappable data, we take the median color of that
data.
- If the axes has no mappable data, we take the facecolor of the
axes.
- If the axes is transparent, we take the facecolor of the figure.
From the resulting color, we calculate the luminance by RGB->YIQ
conversion. Luminance above threshold is considered light, and should
therefore produce a dark highlight. Luminance below threshold is
considered dark, and should produce a bright highlight.
"""
mappable = None
for child in ax.get_children():
if isinstance(child, cm.ScalarMappable) and child.get_array() is not None:
mappable = child
break
if mappable is not None:
data = mappable.get_array()
# Calculate median, ignoring NaNs
median_val = np.nanmedian(np.asarray(data))
# Map through the normalization and colormap
normed_val = mappable.norm(median_val)
rgba = mappable.get_cmap()(normed_val)
else:
# If there's no mappable data, get the axes facecolor
rgba = ax.get_facecolor()
# And if the axes is transparent, pull from the figure
if len(rgba) == 4 and rgba[3] == 0.0:
rgba = ax.figure.get_facecolor()
# Calculate relative luminance
luminance = colorsys.rgb_to_yiq(*rgba[:3])[0]
return luminance <= luminance_threshold
[docs]
def highlight(
*,
artist: Artist | None = None,
ax: mplaxes.Axes | None = None,
color: ColorType | None = None,
bright_color: ColorType = "white",
dark_color: ColorType = "black",
luminance_threshold: float = 0.5,
**kwargs: Any,
) -> list[mpe.AbstractPathEffect]:
"""Apply a contrasting highlight effect to a matplotlib artist.
This is primarily useful for providing contrast between an artist
(e.g., a line plot) and an underlying image (e.g., a spectrogram or scatter plot).
For example, if the underlying image is predominantly dark (under the choice of colormap),
then a bright highlight (default "white") should be used.
If the underlying image is predominantly bright, then a dark highlight (default "black")
should be used.
This function is designed to automatically infer which kind of highlight should be applied
based on the contents of the `ax` axes object, if any. If no color-mapped data can be
identified on `ax`, then the axes facecolor or figure facecolor will be used as fallbacks.
If an `artist` is provided, the highlight effect will be applied in-place, but this is
optional. (See examples below.)
The choices for bright and dark highlight colors, as well as the luminance threshold for
determining which to use, can be customized via the `bright_color`, `dark_color`, and
`luminance_threshold` parameters.
Alternatively, the user can bypass the automatic color inference and directly specify a
highlight color via the `color` parameter.
Parameters
----------
artist : matplotlib.artist.Artist, optional
The artist to which the highlight effect should be applied. If not provided, the
function will still return the appropriate path effect object(s) based on the contents
of `ax`, but will not apply them to any artist.
ax : matplotlib.axes.Axes, optional
The axes to inspect for color-mapped data to determine the appropriate highlight color.
If not provided, the function will attempt to infer an appropriate axes object from
`artist`, and if that fails, will default to the current axes (`plt.gca()`).
color : color specifier, optional
A color specification to use directly for the highlight, bypassing the automatic color
inference. If not provided, the function will determine whether to use `bright_color`
or `dark_color` based on the contents of `ax` and the `luminance_threshold`.
bright_color : color specifier, default 'white'
The color to use for the highlight if the underlying axes is determined to be dark.
dark_color : color specifier, default 'black'
The color to use for the highlight if the underlying axes is determined to be bright.
luminance_threshold : float, default 0.5
The luminance threshold for determining whether the underlying axes is considered bright or dark.
Luminance is calculated by converting the relevant color to YIQ color space and taking
the Y (luminance) component. If the luminance is above this threshold, the axes is
considered bright and `dark_color` will be used for the highlight. If the luminance is
below this threshold, the axes is considered dark and `bright_color` will be used for
the highlight.
**kwargs : dict
Additional keyword arguments to pass to `matplotlib.patheffects.withStroke` when
creating the highlight effect. Common options include `linewidth` (default to 2) and
`alpha` (default to 1.0).
.. note:: `foreground`, if provided, will override the `color` parameter and the
automatic color inference. To avoid confusion, it's recommended to specify highlight
color via the `color` parameter and not to provide `foreground` in `kwargs`.
Returns
-------
effects : list of matplotlib.patheffects.AbstractPathEffect
A list of path effect objects that implement the highlight. If `artist` was provided,
these effects will have been applied to the artist in-place. If `artist` was not
provided, these effects can be applied to any artist via `artist.set_path_effects(effects)`.
Examples
--------
Plotting an f₀ contour with and without highlighting, in bright or dark colormaps
>>> import matplotlib.pyplot as plt
>>> y, sr = librosa.loadx('trumpet')
>>> f0, _, _ = librosa.pyin(y, fmin=100, fmax=1000)
>>> times = librosa.times_like(f0)
>>> D = librosa.stft(y)
>>> fig, ax = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True)
>>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[0, 0],
... vscale='dBFS')
>>> ax[0, 0].plot(times, f0)
>>> ax[0, 0].set_title('Dark image, no highlight')
>>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[0, 1],
... vscale='dBFS')
>>> line = ax[0, 1].plot(times, f0)[0] # 'plot' returns a list of artists
>>> librosa.display.highlight(artist=line)
>>> ax[0, 1].set_title('Dark image, highlighted')
>>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[1, 0],
... vscale='dBFS', cmap='gray_r')
>>> ax[1, 0].plot(times, f0)
>>> ax[1, 0].set_title('Bright image, no highlight')
>>> librosa.display.specshow(D, x_axis='time', y_axis='log_oct3', ax=ax[1, 1],
... vscale='dBFS', cmap='gray_r')
>>> # We can also construct the highlight first and then supply it to the plot command
>>> hl = librosa.display.highlight(ax=ax[1, 1])
>>> ax[1, 1].plot(times, f0, path_effects=hl)
>>> ax[1, 1].set_title('Bright image, highlighted')
>>> for a in ax.flat:
... a.label_outer()
>>> plt.show()
"""
# 1. Resolve Axes
if ax is None:
if artist is not None and hasattr(artist, "axes") and artist.axes is not None:
ax = cast("mplaxes.Axes", artist.axes)
else:
ax = plt.gca()
# 2. Infer highlight color
color = kwargs.pop("foreground", color)
if color is None:
if _get_ax_bright_highlight(ax, luminance_threshold):
# Axes is dark, so we want a bright highlight
stroke_color = bright_color
else:
# Axes is bright, so we want a dark highlight
stroke_color = dark_color
else:
# Use the user-specified highlight color
stroke_color = color
kwargs.setdefault("linewidth", 2)
kwargs.setdefault("alpha", 1.0)
# 3. Create and apply the effect
effects: list[mpe.AbstractPathEffect] = [mpe.withStroke(foreground=stroke_color, **kwargs)]
if artist is not None:
artist.set_path_effects(effects)
return effects