Repository: aeye-lab/pymovements
Pull request #1081 (2025-04-08 16:54:31)

depr: deprecate `utils.plotting`
## :warning: Deprecation

`utils.plotting` is deprecated. draw_image_stimulus() please use `ImageStimulus.show()` instead.
`utils.plotting` will be removed in pymovements v0.27.0

## Description

Deprecate `utils.plotting` in favor of `ImageStimulus.show()`

The following warnings are printed:

```
DeprecationWarning: Call to deprecated function (or staticmethod) draw_image_stimulus(). (Please use ImageStimulus.show() instead. This function will be removed in v0.27.0.) -- Deprecated since version v0.27.0.
```

## Implemented changes

- [x] moved `draw_image_stimulus()` from `utils/plotting.py` to `stimulus/image.py`
- [x] moved remaining logic from `utils/plotting.py` to `plotting/_matplotlib.py`
- [x] update usage in `plotting`
- [x] created deprecated alias functions in `utils/plotting.py`


## How Has This Been Tested?

- [x] moved some tests to `image_plotting_test.py`
- [x] add test that functions are correctly deprecated
- [x] add test that functions are removed in planned version

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings

# Context

##### related issues:
- #999 

##### requires:
- #1094

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
- [x] I have checked my code and corrected any misspellings



Patch:
diff --git a/src/pymovements/plotting/_matplotlib.py b/src/pymovements/plotting/_matplotlib.py
new file mode 100644
index 0000000..fe0b4ab
--- /dev/null
+++ b/src/pymovements/plotting/_matplotlib.py
@@ -0,0 +1,262 @@
+# Copyright (c) 2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Private helper functions for matplotlib."""
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Literal
+from typing import Union
+
+import matplotlib.colors
+import matplotlib.pyplot
+import numpy as np
+import PIL.Image
+from matplotlib.collections import LineCollection
+from typing_extensions import TypeAlias
+
+
+LinearSegmentedColormapType: TypeAlias = dict[
+    Literal['red', 'green', 'blue', 'alpha'],
+    Sequence[tuple[float, ...]],
+]
+
+DEFAULT_SEGMENTDATA: LinearSegmentedColormapType = {
+    'red': [
+        (0.0, 0.0, 0.0),
+        (0.5, 1.0, 1.0),
+        (1.0, 1.0, 1.0),
+    ],
+    'green': [
+        (0.0, 0.0, 0.0),
+        (0.5, 1.0, 1.0),
+        (1.0, 0.0, 0.0),
+    ],
+    'blue': [
+        (0.0, 0.0, 0.0),
+        (0.5, 0.0, 0.0),
+        (1.0, 0.0, 0.0),
+    ],
+}
+
+DEFAULT_SEGMENTDATA_TWOSLOPE: LinearSegmentedColormapType = {
+    'red': [
+        (0.0, 0.0, 0.0),
+        (0.5, 0.0, 0.0),
+        (0.75, 1.0, 1.0),
+        (1.0, 1.0, 1.0),
+    ],
+    'green': [
+        (0.0, 0.0, 0.0),
+        (0.25, 1.0, 1.0),
+        (0.5, 0.0, 0.0),
+        (0.75, 1.0, 1.0),
+        (1.0, 0.0, 0.0),
+    ],
+    'blue': [
+        (0.0, 1.0, 1.0),
+        (0.25, 1.0, 1.0),
+        (0.5, 0.0, 0.0),
+        (1.0, 0.0, 0.0),
+    ],
+}
+
+CmapNormType: TypeAlias = Union[
+    matplotlib.colors.TwoSlopeNorm,
+    matplotlib.colors.Normalize,
+    matplotlib.colors.NoNorm,
+]
+
+MatplotlibSetupType: TypeAlias = tuple[
+    matplotlib.pyplot.figure,
+    matplotlib.pyplot.Axes,
+    matplotlib.colors.Colormap,
+    CmapNormType,
+    np.ndarray,
+    bool,
+]
+
+
+def _setup_matplotlib(
+        x_signal: np.ndarray,
+        y_signal: np.ndarray,
+        figsize: tuple[int, int],
+        cmap: matplotlib.colors.Colormap | None = None,
+        cmap_norm: matplotlib.colors.Normalize | str | None = None,
+        cmap_segmentdata: LinearSegmentedColormapType | None = None,
+        cval: np.ndarray | None = None,
+        show_cbar: bool = False,
+        add_stimulus: bool = False,
+        path_to_image_stimulus: str | None = None,
+        stimulus_origin: str = 'upper',
+        padding: float | None = None,
+        pad_factor: float | None = 0.05,
+) -> MatplotlibSetupType:
+    """Configure cmap.
+
+    Parameters
+    ----------
+    x_signal: np.ndarray
+        Time-step array.
+    y_signal: np.ndarray
+        Time-step array.
+    figsize: tuple[int, int]
+        Figure size.
+    cmap: matplotlib.colors.Colormap | None
+        Color map for line color values. (default: None)
+    cmap_norm: matplotlib.colors.Normalize | str | None
+        Normalization for color values. (default: None)
+    cmap_segmentdata: LinearSegmentedColormapType | None
+        Color map segmentation to build color map. (default: None)
+    cval: np.ndarray | None
+        Line color values. (default: None)
+    show_cbar: bool
+        Shows color bar if True. (default: False)
+    add_stimulus: bool
+        Boolean value indicationg whether to plot the scanpath on the stimuls. (default: False)
+    path_to_image_stimulus: str | None
+        Path of the stimulus to be shown. (default: None)
+    stimulus_origin: str
+        Origin of stimuls to plot on the stimulus. (default: 'upper')
+    padding: float | None
+        Absolute padding value. If None it is inferred from pad_factor and limits. (default: None)
+    pad_factor: float | None
+        Relative padding factor to construct padding from value. (default: 0.05)
+
+    Returns
+    -------
+    MatplotlibSetupType
+        Configures fig, ax, cmap, cmap_norm, cmap_segmentdata, cval, and show_cbar.
+    """
+    n = len(x_signal)
+
+    fig = matplotlib.pyplot.figure(figsize=figsize)
+    ax = fig.gca()
+
+    if add_stimulus:
+        img = PIL.Image.open(path_to_image_stimulus)
+        ax.imshow(img, origin=stimulus_origin, extent=None)
+    else:
+        if padding is None:
+            x_pad = (np.nanmax(x_signal) - np.nanmin(x_signal)) * pad_factor
+            y_pad = (np.nanmax(y_signal) - np.nanmin(y_signal)) * pad_factor
+        else:
+            x_pad = padding
+            y_pad = padding
+
+        ax.set_xlim(np.nanmin(x_signal) - x_pad, np.nanmax(x_signal) + x_pad)
+        ax.set_ylim(np.nanmin(y_signal) - y_pad, np.nanmax(y_signal) + y_pad)
+        ax.invert_yaxis()
+
+    if cval is None:
+        cval = np.zeros(n)
+        show_cbar = False
+
+    cval_max = np.nanmax(np.abs(cval))
+    cval_min = np.nanmin(cval).astype(float)
+
+    if cmap_norm is None:
+        if cval_max and cval_min < 0:
+            cmap_norm = 'twoslope'
+        elif cval_max:
+            cmap_norm = 'normalize'
+        else:
+            cmap_norm = 'nonorm'
+
+    if cmap is None:
+        if cmap_segmentdata is None:
+            if cmap_norm == 'twoslope':
+                cmap_segmentdata = DEFAULT_SEGMENTDATA_TWOSLOPE
+            else:
+                cmap_segmentdata = DEFAULT_SEGMENTDATA
+
+        cmap = matplotlib.colors.LinearSegmentedColormap(
+            'line_cmap', segmentdata=cmap_segmentdata, N=512,
+        )
+
+    if cmap_norm == 'twoslope':
+        cmap_norm = matplotlib.colors.TwoSlopeNorm(
+            vcenter=0, vmin=-cval_max, vmax=cval_max,
+        )
+    elif cmap_norm == 'normalize':
+        cmap_norm = matplotlib.colors.Normalize(
+            vmin=cval_min, vmax=cval_max,
+        )
+    elif cmap_norm == 'nonorm':
+        cmap_norm = matplotlib.colors.NoNorm()
+
+    elif isinstance(cmap_norm, str):
+        # pylint: disable=protected-access
+
+        # to handle after https://github.com/pydata/xarray/pull/8030 is merged
+        if (
+            scale_class := matplotlib.scale._scale_mapping.get(cmap_norm, None)  # type: ignore
+        ) is None:
+            raise ValueError(f'cmap_norm string {cmap_norm} is not supported')
+
+        norm_class = matplotlib.colors.make_norm_from_scale(scale_class)
+        cmap_norm = norm_class(matplotlib.colors.Normalize)()
+
+    return fig, ax, cmap, cmap_norm, cval, show_cbar
+
+
+def _draw_line_data(
+        x_signal: np.ndarray,
+        y_signal: np.ndarray,
+        ax: matplotlib.pyplot.Axes,
+        cmap: matplotlib.colors.Colormap | None = None,
+        cmap_norm: matplotlib.colors.Normalize | str | None = None,
+        cval: np.ndarray | None = None,
+) -> matplotlib.pyplot.Axes:
+    """Draw line data.
+
+    Parameters
+    ----------
+    x_signal: np.ndarray
+        Data to be plotted.
+    y_signal: np.ndarray
+        Data to be plotted.
+    ax: matplotlib.pyplot.Axes
+        Matplotlib axes.
+    cmap: matplotlib.colors.Colormap | None
+        Color map for line color values. (default: None)
+    cmap_norm: matplotlib.colors.Normalize | str | None
+        Normalization for color values. (default: None)
+    cval: np.ndarray | None
+        Line color values. (default: None)
+
+    Returns
+    -------
+    matplotlib.pyplot.Axes
+        Axes with added line data.
+
+    """
+    # Create a set of line segments so that we can color them individually
+    # This creates the points as a N x 1 x 2 array so that we can stack points
+    # together easily to get the segments. The segments array for line collection
+    # needs to be (numlines) x (points per line) x 2 (for x and y)
+    points = np.array([x_signal, y_signal]).T.reshape((-1, 1, 2))
+    segments = np.concatenate([points[:-1], points[1:]], axis=1)
+    # Create a continuous norm to map from data points to colors
+    line_collection = LineCollection(segments, cmap=cmap, norm=cmap_norm)
+    # Set the values used for colormapping
+    line_collection.set_array(cval)
+    line_collection.set_linewidth(2)
+    line = ax.add_collection(line_collection)
+    return line
diff --git a/src/pymovements/plotting/heatmap.py b/src/pymovements/plotting/heatmap.py
index 1df4589..50c3c61 100644
--- a/src/pymovements/plotting/heatmap.py
+++ b/src/pymovements/plotting/heatmap.py
@@ -27,7 +27,7 @@ import numpy as np
 from matplotlib import colors
 
 from pymovements.gaze import GazeDataFrame
-from pymovements.utils.plotting import draw_image_stimulus
+from pymovements.stimulus.image import _draw_image_stimulus
 
 
 def heatmap(
@@ -158,7 +158,7 @@ def heatmap(
     # Create the plot
     if add_stimulus:
         assert path_to_image_stimulus
-        fig, ax = draw_image_stimulus(
+        fig, ax = _draw_image_stimulus(
             path_to_image_stimulus,
             origin=stimulus_origin,
             figsize=figsize,
diff --git a/src/pymovements/plotting/scanpathplot.py b/src/pymovements/plotting/scanpathplot.py
index c78cbb4..709883c 100644
--- a/src/pymovements/plotting/scanpathplot.py
+++ b/src/pymovements/plotting/scanpathplot.py
@@ -31,10 +31,9 @@ from matplotlib.patches import Circle
 
 from pymovements.events import EventDataFrame
 from pymovements.gaze import GazeDataFrame
-from pymovements.utils.plotting import draw_line_data
-from pymovements.utils.plotting import LinearSegmentedColormapType
-from pymovements.utils.plotting import setup_matplotlib
-
+from pymovements.plotting._matplotlib import _draw_line_data
+from pymovements.plotting._matplotlib import _setup_matplotlib
+from pymovements.plotting._matplotlib import LinearSegmentedColormapType
 
 # This is really a dirty workaround to use the Agg backend if runnning pytest.
 # This is needed as Windows workers on GitHub fail randomly with other backends.
@@ -127,7 +126,7 @@ def scanpathplot(
     x_signal = events.frame[position_column].list.get(0)
     y_signal = events.frame[position_column].list.get(1)
 
-    fig, ax, cmap, cmap_norm, cval, show_cbar = setup_matplotlib(
+    fig, ax, cmap, cmap_norm, cval, show_cbar = _setup_matplotlib(
         x_signal,
         y_signal,
         figsize,
@@ -158,7 +157,7 @@ def scanpathplot(
         assert gaze
         gaze_x_signal = gaze.frame[gaze_position_column].list.get(0)
         gaze_y_signal = gaze.frame[gaze_position_column].list.get(1)
-        line = draw_line_data(
+        line = _draw_line_data(
             gaze_x_signal,
             gaze_y_signal,
             ax,
diff --git a/src/pymovements/plotting/traceplot.py b/src/pymovements/plotting/traceplot.py
index 1938d61..c424f86 100644
--- a/src/pymovements/plotting/traceplot.py
+++ b/src/pymovements/plotting/traceplot.py
@@ -28,10 +28,9 @@ import matplotlib.scale
 import numpy as np
 
 from pymovements.gaze.gaze_dataframe import GazeDataFrame
-from pymovements.utils.plotting import draw_line_data
-from pymovements.utils.plotting import LinearSegmentedColormapType
-from pymovements.utils.plotting import setup_matplotlib
-
+from pymovements.plotting._matplotlib import _draw_line_data
+from pymovements.plotting._matplotlib import _setup_matplotlib
+from pymovements.plotting._matplotlib import LinearSegmentedColormapType
 
 # This is really a dirty workaround to use the Agg backend if runnning pytest.
 # This is needed as Windows workers on GitHub fail randomly with other backends.
@@ -109,7 +108,7 @@ def traceplot(
     x_signal = gaze.frame[position_column].list.get(0)
     y_signal = gaze.frame[position_column].list.get(1)
 
-    fig, ax, cmap, cmap_norm, cval, show_cbar = setup_matplotlib(
+    fig, ax, cmap, cmap_norm, cval, show_cbar = _setup_matplotlib(
         x_signal,
         y_signal,
         figsize,
@@ -125,7 +124,7 @@ def traceplot(
         pad_factor,
     )
 
-    line = draw_line_data(
+    line = _draw_line_data(
         x_signal,
         y_signal,
         ax,
diff --git a/src/pymovements/stimulus/image.py b/src/pymovements/stimulus/image.py
index 9c2cbed..712ccfd 100644
--- a/src/pymovements/stimulus/image.py
+++ b/src/pymovements/stimulus/image.py
@@ -22,9 +22,11 @@ from __future__ import annotations
 
 from pathlib import Path
 
+import matplotlib.pyplot
+import PIL.Image
+
 from pymovements._utils._paths import get_filepaths
 from pymovements._utils._strings import curly_to_regex
-from pymovements.utils.plotting import draw_image_stimulus
 
 
 class ImageStimulus:
@@ -49,7 +51,7 @@ class ImageStimulus:
         origin: str
             Origin of the stimulus to be shown.
         """
-        draw_image_stimulus(self.images[stimulus_id], origin=origin, show=True)
+        _draw_image_stimulus(self.images[stimulus_id], origin=origin, show=True)
 
 
 def from_file(image_path: str | Path) -> ImageStimulus:
@@ -95,3 +97,46 @@ def from_files(path: str | Path, filename_format: str) -> ImageStimulus:
         image_stimuli.append(filename)
 
     return ImageStimulus(image_stimuli)
+
+
+def _draw_image_stimulus(
+        image_stimulus: str | Path,
+        origin: str = 'upper',
+        show: bool = False,
+        figsize: tuple[float, float] = (15, 10),
+        extent: list[float] | None = None,
+        fig: matplotlib.pyplot.figure | None = None,
+        ax: matplotlib.pyplot.Axes | None = None,
+) -> tuple[matplotlib.pyplot.figure, matplotlib.pyplot.Axes]:
+    """Draw stimulus.
+
+    Parameters
+    ----------
+    image_stimulus: str | Path
+        Path to image stimulus.
+    origin: str
+        Origin how to draw the image.
+    show: bool
+        Boolean whether to show the image. (default: False)
+    figsize: tuple[float, float]
+        Size of the figure. (default: (15, 10))
+    extent: list[float] | None
+        Extent of image. (default: None)
+    fig: matplotlib.pyplot.figure | None
+        Matplotlib canvas. (default: None)
+    ax: matplotlib.pyplot.Axes | None
+        Matplotlib axes. (default: None)
+
+    Returns
+    -------
+    fig: matplotlib.pyplot.figure
+    ax: matplotlib.pyplot.Axes
+    """
+    img = PIL.Image.open(image_stimulus)
+    if not fig:
+        fig, ax = matplotlib.pyplot.subplots(figsize=figsize)
+    assert ax
+    ax.imshow(img, origin=origin, extent=extent)
+    if show:
+        matplotlib.pyplot.show()
+    return fig, ax
diff --git a/src/pymovements/utils/__init__.py b/src/pymovements/utils/__init__.py
index 3d526cb..e7a6184 100644
--- a/src/pymovements/utils/__init__.py
+++ b/src/pymovements/utils/__init__.py
@@ -20,6 +20,9 @@
 # pylint: disable=cyclic-import
 """Provides utility functions.
 
+.. deprecated:: v0.22.0
+   This module will be removed in v0.27.0.
+
 .. rubric:: Modules
 
 .. autosummary::
diff --git a/src/pymovements/utils/plotting.py b/src/pymovements/utils/plotting.py
index 087789a..b71e9dd 100644
--- a/src/pymovements/utils/plotting.py
+++ b/src/pymovements/utils/plotting.py
@@ -17,82 +17,31 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
-"""Functions for plotting."""
+"""Functions for plotting.
+
+.. deprecated:: v0.22.0
+   This module will be removed in v0.27.0.
+"""
 from __future__ import annotations
 
-from collections.abc import Sequence
 from pathlib import Path
-from typing import Literal
-from typing import Union
 
+import matplotlib.colors
 import matplotlib.pyplot
 import numpy as np
-import PIL.Image
-from matplotlib.collections import LineCollection
-from typing_extensions import TypeAlias
-
-LinearSegmentedColormapType: TypeAlias = dict[
-    Literal['red', 'green', 'blue', 'alpha'],
-    Sequence[tuple[float, ...]],
-]
-
-
-DEFAULT_SEGMENTDATA: LinearSegmentedColormapType = {
-    'red': [
-        (0.0, 0.0, 0.0),
-        (0.5, 1.0, 1.0),
-        (1.0, 1.0, 1.0),
-    ],
-    'green': [
-        (0.0, 0.0, 0.0),
-        (0.5, 1.0, 1.0),
-        (1.0, 0.0, 0.0),
-    ],
-    'blue': [
-        (0.0, 0.0, 0.0),
-        (0.5, 0.0, 0.0),
-        (1.0, 0.0, 0.0),
-    ],
-}
+from deprecated.sphinx import deprecated
 
-
-DEFAULT_SEGMENTDATA_TWOSLOPE: LinearSegmentedColormapType = {
-    'red': [
-        (0.0, 0.0, 0.0),
-        (0.5, 0.0, 0.0),
-        (0.75, 1.0, 1.0),
-        (1.0, 1.0, 1.0),
-    ],
-    'green': [
-        (0.0, 0.0, 0.0),
-        (0.25, 1.0, 1.0),
-        (0.5, 0.0, 0.0),
-        (0.75, 1.0, 1.0),
-        (1.0, 0.0, 0.0),
-    ],
-    'blue': [
-        (0.0, 1.0, 1.0),
-        (0.25, 1.0, 1.0),
-        (0.5, 0.0, 0.0),
-        (1.0, 0.0, 0.0),
-    ],
-}
-
-CmapNormType: TypeAlias = Union[
-    matplotlib.colors.TwoSlopeNorm,
-    matplotlib.colors.Normalize,
-    matplotlib.colors.NoNorm,
-]
-MatplotlibSetupType: TypeAlias = tuple[
-    matplotlib.pyplot.figure,
-    matplotlib.pyplot.Axes,
-    matplotlib.colors.Colormap,
-    CmapNormType,
-    np.ndarray,
-    bool,
-]
+from pymovements.plotting._matplotlib import _draw_line_data
+from pymovements.plotting._matplotlib import _setup_matplotlib
+from pymovements.plotting._matplotlib import LinearSegmentedColormapType
+from pymovements.plotting._matplotlib import MatplotlibSetupType
+from pymovements.stimulus.image import _draw_image_stimulus
 
 
+@deprecated(
+    reason='This function will be removed in v0.27.0.',
+    version='v0.22.0',
+)
 def setup_matplotlib(
         x_signal: np.ndarray,
         y_signal: np.ndarray,
@@ -110,6 +59,9 @@ def setup_matplotlib(
 ) -> MatplotlibSetupType:
     """Configure cmap.
 
+    .. deprecated:: v0.22.0
+       This function will be removed in v0.27.0.
+
     Parameters
     ----------
     x_signal: np.ndarray
@@ -144,78 +96,28 @@ def setup_matplotlib(
     MatplotlibSetupType
         Configures fig, ax, cmap, cmap_norm, cmap_segmentdata, cval, and show_cbar.
     """
-    n = len(x_signal)
-
-    fig = matplotlib.pyplot.figure(figsize=figsize)
-    ax = fig.gca()
-
-    if add_stimulus:
-        img = PIL.Image.open(path_to_image_stimulus)
-        ax.imshow(img, origin=stimulus_origin, extent=None)
-    else:
-        if padding is None:
-            x_pad = (np.nanmax(x_signal) - np.nanmin(x_signal)) * pad_factor
-            y_pad = (np.nanmax(y_signal) - np.nanmin(y_signal)) * pad_factor
-        else:
-            x_pad = padding
-            y_pad = padding
-
-        ax.set_xlim(np.nanmin(x_signal) - x_pad, np.nanmax(x_signal) + x_pad)
-        ax.set_ylim(np.nanmin(y_signal) - y_pad, np.nanmax(y_signal) + y_pad)
-        ax.invert_yaxis()
-
-    if cval is None:
-        cval = np.zeros(n)
-        show_cbar = False
-
-    cval_max = np.nanmax(np.abs(cval))
-    cval_min = np.nanmin(cval).astype(float)
-
-    if cmap_norm is None:
-        if cval_max and cval_min < 0:
-            cmap_norm = 'twoslope'
-        elif cval_max:
-            cmap_norm = 'normalize'
-        else:
-            cmap_norm = 'nonorm'
-
-    if cmap is None:
-        if cmap_segmentdata is None:
-            if cmap_norm == 'twoslope':
-                cmap_segmentdata = DEFAULT_SEGMENTDATA_TWOSLOPE
-            else:
-                cmap_segmentdata = DEFAULT_SEGMENTDATA
-
-        cmap = matplotlib.colors.LinearSegmentedColormap(
-            'line_cmap', segmentdata=cmap_segmentdata, N=512,
-        )
-
-    if cmap_norm == 'twoslope':
-        cmap_norm = matplotlib.colors.TwoSlopeNorm(
-            vcenter=0, vmin=-cval_max, vmax=cval_max,
-        )
-    elif cmap_norm == 'normalize':
-        cmap_norm = matplotlib.colors.Normalize(
-            vmin=cval_min, vmax=cval_max,
-        )
-    elif cmap_norm == 'nonorm':
-        cmap_norm = matplotlib.colors.NoNorm()
-
-    elif isinstance(cmap_norm, str):
-        # pylint: disable=protected-access
-
-        # to handle after https://github.com/pydata/xarray/pull/8030 is merged
-        if (
-            scale_class := matplotlib.scale._scale_mapping.get(cmap_norm, None)  # type: ignore
-        ) is None:
-            raise ValueError(f'cmap_norm string {cmap_norm} is not supported')
-
-        norm_class = matplotlib.colors.make_norm_from_scale(scale_class)
-        cmap_norm = norm_class(matplotlib.colors.Normalize)()
-
-    return fig, ax, cmap, cmap_norm, cval, show_cbar
-
-
+    return _setup_matplotlib(
+        x_signal=x_signal,
+        y_signal=y_signal,
+        figsize=figsize,
+        cmap=cmap,
+        cmap_norm=cmap_norm,
+        cmap_segmentdata=cmap_segmentdata,
+        cval=cval,
+        show_cbar=show_cbar,
+        add_stimulus=add_stimulus,
+        path_to_image_stimulus=path_to_image_stimulus,
+        stimulus_origin=stimulus_origin,
+        padding=padding,
+        pad_factor=pad_factor,
+    )
+
+
+@deprecated(
+    reason='Please use ImageStimulus.show() instead. '
+           'This function will be removed in v0.27.0.',
+    version='v0.22.0',
+)
 def draw_image_stimulus(
         image_stimulus: str | Path,
         origin: str = 'upper',
@@ -227,6 +129,10 @@ def draw_image_stimulus(
 ) -> tuple[matplotlib.pyplot.figure, matplotlib.pyplot.Axes]:
     """Draw stimulus.
 
+    .. deprecated:: v0.22.0
+       Please use :py:meth:`~pymovements.ImageStimulus.show()` instead.
+       This function will be removed in v0.27.0.
+
     Parameters
     ----------
     image_stimulus: str | Path
@@ -249,16 +155,21 @@ def draw_image_stimulus(
     fig: matplotlib.pyplot.figure
     ax: matplotlib.pyplot.Axes
     """
-    img = PIL.Image.open(image_stimulus)
-    if not fig:
-        fig, ax = matplotlib.pyplot.subplots(figsize=figsize)
-    assert ax
-    ax.imshow(img, origin=origin, extent=extent)
-    if show:
-        matplotlib.pyplot.show()
-    return fig, ax
-
-
+    return _draw_image_stimulus(
+        image_stimulus=image_stimulus,
+        origin=origin,
+        show=show,
+        figsize=figsize,
+        extent=extent,
+        fig=fig,
+        ax=ax,
+    )
+
+
+@deprecated(
+    reason='This function will be removed in v0.27.0.',
+    version='v0.22.0',
+)
 def draw_line_data(
         x_signal: np.ndarray,
         y_signal: np.ndarray,
@@ -269,6 +180,9 @@ def draw_line_data(
 ) -> matplotlib.pyplot.Axes:
     """Draw line data.
 
+    .. deprecated:: v0.22.0
+       This function will be removed in v0.27.0.
+
     Parameters
     ----------
     x_signal: np.ndarray
@@ -288,18 +202,12 @@ def draw_line_data(
     -------
     matplotlib.pyplot.Axes
         Axes with added line data.
-
     """
-    # Create a set of line segments so that we can color them individually
-    # This creates the points as a N x 1 x 2 array so that we can stack points
-    # together easily to get the segments. The segments array for line collection
-    # needs to be (numlines) x (points per line) x 2 (for x and y)
-    points = np.array([x_signal, y_signal]).T.reshape((-1, 1, 2))
-    segments = np.concatenate([points[:-1], points[1:]], axis=1)
-    # Create a continuous norm to map from data points to colors
-    line_collection = LineCollection(segments, cmap=cmap, norm=cmap_norm)
-    # Set the values used for colormapping
-    line_collection.set_array(cval)
-    line_collection.set_linewidth(2)
-    line = ax.add_collection(line_collection)
-    return line
+    return _draw_line_data(
+        x_signal=x_signal,
+        y_signal=y_signal,
+        ax=ax,
+        cmap=cmap,
+        cmap_norm=cmap_norm,
+        cval=cval,
+    )


Test patch:
diff --git a/tests/unit/stimulus/image_plotting_test.py b/tests/unit/stimulus/image_plotting_test.py
new file mode 100644
index 0000000..995e6dd
--- /dev/null
+++ b/tests/unit/stimulus/image_plotting_test.py
@@ -0,0 +1,136 @@
+# Copyright (c) 2023-2025 The pymovements Project Authors
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+"""Test pymovements plotting utils."""
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+from matplotlib import pyplot
+
+from pymovements.stimulus.image import _draw_image_stimulus
+
+
+@pytest.mark.parametrize(
+    ('image_stimulus'),
+    (
+        pytest.param(
+            './tests/files/pexels-zoorg-1000498.jpg',
+            id='image_stimulus_str',
+        ),
+        pytest.param(
+            Path('./tests/files/pexels-zoorg-1000498.jpg'),
+            id='image_stimulus_Path',
+        ),
+    ),
+)
+@pytest.mark.parametrize(
+    ('origin'),
+    (
+        pytest.param(
+            'upper',
+            id='origin_upper',
+        ),
+        pytest.param(
+            'lower',
+            id='origin_lower',
+        ),
+    ),
+)
+def test_show_image_stimulus(image_stimulus, origin, monkeypatch):
+    mock = Mock()
+    monkeypatch.setattr(pyplot, 'show', mock)
+    _draw_image_stimulus(image_stimulus, origin=origin, show=True)
+    pyplot.close()
+    mock.assert_called_once()
+
+
+@pytest.mark.parametrize(
+    ('image_stimulus'),
+    (
+        pytest.param(
+            './tests/files/pexels-zoorg-1000498.jpg',
+            id='image_stimulus_str',
+        ),
+        pytest.param(
+            Path('./tests/files/pexels-zoorg-1000498.jpg'),
+            id='image_stimulus_Path',
+        ),
+    ),
+)
+@pytest.mark.parametrize(
+    ('origin'),
+    (
+        pytest.param(
+            'upper',
+            id='origin_upper',
+        ),
+        pytest.param(
+            'lower',
+            id='origin_lower',
+        ),
+    ),
+)
+def test_no_show_image_stimulus(image_stimulus, origin, monkeypatch):
+    mock = Mock()
+    monkeypatch.setattr(pyplot, 'show', mock)
+    _draw_image_stimulus(image_stimulus, origin=origin, show=False)
+    pyplot.close()
+    mock.assert_not_called()
+
+
+@pytest.mark.parametrize(
+    ('image_stimulus'),
+    (
+        pytest.param(
+            './tests/files/pexels-zoorg-1000498.jpg',
+            id='image_stimulus_str',
+        ),
+        pytest.param(
+            Path('./tests/files/pexels-zoorg-1000498.jpg'),
+            id='image_stimulus_Path',
+        ),
+    ),
+)
+@pytest.mark.parametrize(
+    ('origin'),
+    (
+        pytest.param(
+            'upper',
+            id='origin_upper',
+        ),
+        pytest.param(
+            'lower',
+            id='origin_lower',
+        ),
+    ),
+)
+def test_image_stimulus_fig_not_None(image_stimulus, origin, monkeypatch):
+    mock = Mock()
+    monkeypatch.setattr(pyplot, 'show', mock)
+    fig, ax = pyplot.subplots(figsize=(15, 10))
+    _draw_image_stimulus(
+        image_stimulus,
+        origin=origin,
+        fig=fig,
+        ax=ax,
+        show=True,
+    )
+    pyplot.close()
+    mock.assert_called_once()
diff --git a/tests/unit/utils/plotting_test.py b/tests/unit/utils/plotting_test.py
index 7861be6..2fd29f1 100644
--- a/tests/unit/utils/plotting_test.py
+++ b/tests/unit/utils/plotting_test.py
@@ -18,119 +18,141 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 """Test pymovements plotting utils."""
-from pathlib import Path
-from unittest.mock import Mock
+import re
 
+import matplotlib.pyplot
+import numpy as np
 import pytest
-from matplotlib import pyplot
 
-import pymovements as pm
+from pymovements import __version__
+from pymovements.utils.plotting import draw_image_stimulus
+from pymovements.utils.plotting import draw_line_data
+from pymovements.utils.plotting import setup_matplotlib
 
 
+@pytest.fixture(
+    name='axes',
+    params=[
+        (10, 15),
+        (15, 15),
+    ],
+)
+def axes_fixture(request):
+    fig = matplotlib.pyplot.figure(figsize=request.param)
+    yield fig.gca()
+
+
+@pytest.mark.filterwarnings('ignore::DeprecationWarning')
 @pytest.mark.parametrize(
-    ('image_stimulus'),
-    (
+    'kwargs',
+    [
         pytest.param(
-            './tests/files/pexels-zoorg-1000498.jpg',
-            id='image_stimulus_str',
+            {
+                'x_signal': np.array([0.0, 1.0]),
+                'y_signal': np.array([0.0, 2.0]),
+                'figsize': (10, 15),
+            },
+            id='both_signals_figsize',
         ),
-        pytest.param(
-            Path('./tests/files/pexels-zoorg-1000498.jpg'),
-            id='image_stimulus_Path',
-        ),
-    ),
+    ],
 )
+def test_setup_matplotlib(kwargs):
+    setup_matplotlib(**kwargs)
+
+
+@pytest.mark.filterwarnings('ignore::DeprecationWarning')
 @pytest.mark.parametrize(
-    ('origin'),
-    (
+    ('plotting_function', 'kwargs'),
+    [
         pytest.param(
-            'upper',
-            id='origin_upper',
+            draw_image_stimulus,
+            {
+                'image_stimulus': 'tests/files/pexels-zoorg-1000498.jpg',
+            },
+            id='draw_image_stimulus',
         ),
+
         pytest.param(
-            'lower',
-            id='origin_lower',
+            draw_line_data,
+            {
+                'x_signal': np.array([0.0, 0.0]),
+                'y_signal': np.array([0.0, 0.0]),
+            },
+            id='_draw_line_data',
         ),
-    ),
+    ],
 )
-def test_show_image_stimulus(image_stimulus, origin, monkeypatch):
-    mock = Mock()
-    monkeypatch.setattr(pyplot, 'show', mock)
-    pm.utils.plotting.draw_image_stimulus(image_stimulus, origin=origin, show=True)
-    pyplot.close()
-    mock.assert_called_once()
+def test_plotting_function(plotting_function, kwargs, axes):
+    plotting_function(ax=axes, **kwargs)
 
 
 @pytest.mark.parametrize(
-    ('image_stimulus'),
-    (
+    ('plotting_function', 'kwargs'),
+    [
         pytest.param(
-            './tests/files/pexels-zoorg-1000498.jpg',
-            id='image_stimulus_str',
+            setup_matplotlib,
+            {
+                'x_signal': np.array([0.0, 0.0]),
+                'y_signal': np.array([0.0, 0.0]),
+                'figsize': (10, 15),
+            },
+            id='_setup_matplotlib',
         ),
+
         pytest.param(
-            Path('./tests/files/pexels-zoorg-1000498.jpg'),
-            id='image_stimulus_Path',
-        ),
-    ),
-)
-@pytest.mark.parametrize(
-    ('origin'),
-    (
-        pytest.param(
-            'upper',
-            id='origin_upper',
+            draw_image_stimulus,
+            {},
+            id='draw_image_stimulus',
         ),
+
         pytest.param(
-            'lower',
-            id='origin_lower',
+            draw_line_data,
+            {},
+            id='_draw_line_data',
         ),
-    ),
+    ],
 )
-def test_no_show_image_stimulus(image_stimulus, origin, monkeypatch):
-    mock = Mock()
-    monkeypatch.setattr(pyplot, 'show', mock)
-    pm.utils.plotting.draw_image_stimulus(image_stimulus, origin=origin, show=False)
-    pyplot.close()
-    mock.assert_not_called()
+def test_plotting_function_deprecated(plotting_function, kwargs):
+    with pytest.raises(DeprecationWarning):
+        plotting_function(**kwargs)
 
 
 @pytest.mark.parametrize(
-    ('image_stimulus'),
-    (
+    ('plotting_function', 'kwargs'),
+    [
         pytest.param(
-            './tests/files/pexels-zoorg-1000498.jpg',
-            id='image_stimulus_str',
+            setup_matplotlib,
+            {
+                'x_signal': np.array([0.0, 0.0]),
+                'y_signal': np.array([0.0, 0.0]),
+                'figsize': (10, 15),
+            },
+            id='_setup_matplotlib',
         ),
+
         pytest.param(
-            Path('./tests/files/pexels-zoorg-1000498.jpg'),
-            id='image_stimulus_Path',
-        ),
-    ),
-)
-@pytest.mark.parametrize(
-    ('origin'),
-    (
-        pytest.param(
-            'upper',
-            id='origin_upper',
+            draw_image_stimulus,
+            {},
+            id='draw_image_stimulus',
         ),
+
         pytest.param(
-            'lower',
-            id='origin_lower',
+            draw_line_data,
+            {},
+            id='_draw_line_data',
         ),
-    ),
+    ],
 )
-def test_image_stimulus_fig_not_None(image_stimulus, origin, monkeypatch):
-    mock = Mock()
-    monkeypatch.setattr(pyplot, 'show', mock)
-    fig, ax = pyplot.subplots(figsize=(15, 10))
-    pm.utils.plotting.draw_image_stimulus(
-        image_stimulus,
-        origin=origin,
-        fig=fig,
-        ax=ax,
-        show=True,
+def test_plotting_function_removed(plotting_function, kwargs):
+    with pytest.raises(DeprecationWarning) as info:
+        plotting_function(**kwargs)
+
+    regex = re.compile(r'.*will be removed in v(?P<version>[0-9]*[.][0-9]*[.][0-9]*)[.)].*')
+
+    msg = info.value.args[0]
+    remove_version = regex.match(msg).groupdict()['version']
+    current_version = __version__.split('+')[0]
+    assert current_version < remove_version, (
+        f'utils/filters.py was planned to be removed in v{remove_version}. '
+        f'Current version is v{current_version}.'
     )
-    pyplot.close()
-    mock.assert_called_once()