"""
Scales define the distribution of data values on an axis, e.g. a log scaling.
The mapping is implemented through `.Transform` subclasses.
The following scales are built-in:
.. _builtin_scales:
============= ===================== ================================ =================================
Name Class Transform Inverted transform
============= ===================== ================================ =================================
"asinh" `AsinhScale` `AsinhTransform` `InvertedAsinhTransform`
"function" `FuncScale` `FuncTransform` `FuncTransform`
"functionlog" `FuncScaleLog` `FuncTransform` + `LogTransform` `InvertedLogTransform` + `FuncTransform`
"linear" `LinearScale` `.IdentityTransform` `.IdentityTransform`
"log" `LogScale` `LogTransform` `InvertedLogTransform`
"logit" `LogitScale` `LogitTransform` `LogisticTransform`
"symlog" `SymmetricalLogScale` `SymmetricalLogTransform` `InvertedSymmetricalLogTransform`
============= ===================== ================================ =================================
A user will often only use the scale name, e.g. when setting the scale through
`~.Axes.set_xscale`: ``ax.set_xscale("log")``.
See also the :ref:`scales examples ` in the documentation.
Custom scaling can be achieved through `FuncScale`, or by creating your own
`ScaleBase` subclass and corresponding transforms (see :doc:`/gallery/scales/custom_scale`).
Third parties can register their scales by name through `register_scale`.
""" # noqa: E501
import inspect
import textwrap
from functools import wraps
import numpy as np
import matplotlib as mpl
from matplotlib import _api, _docstring
from matplotlib.ticker import (
NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
SymmetricalLogLocator, AsinhLocator, LogitLocator)
from matplotlib.transforms import Transform, IdentityTransform
class ScaleBase:
"""
The base class for all scales.
Scales are separable transformations, working on a single dimension.
Subclasses should override
:attr:`!name`
The scale's name.
:meth:`get_transform`
A method returning a `.Transform`, which converts data coordinates to
scaled coordinates. This transform should be invertible, so that e.g.
mouse positions can be converted back to data coordinates.
:meth:`set_default_locators_and_formatters`
A method that sets default locators and formatters for an `~.axis.Axis`
that uses this scale.
:meth:`limit_range_for_scale`
An optional method that "fixes" the axis range to acceptable values,
e.g. restricting log-scaled axes to positive values.
"""
def __init__(self, axis):
r"""
Construct a new scale.
Notes
-----
The following note is for scale implementers.
For back-compatibility reasons, scales take an `~matplotlib.axis.Axis`
object as the first argument.
.. deprecated:: 3.11
The *axis* parameter is now optional, i.e. matplotlib is compatible
with `.ScaleBase` subclasses that do not take an *axis* parameter.
The *axis* parameter is pending-deprecated. It will be deprecated
in matplotlib 3.13, and removed in matplotlib 3.15.
3rd-party scales are recommended to remove the *axis* parameter now
if they can afford to restrict compatibility to matplotlib >= 3.11
already. Otherwise, they may keep the *axis* parameter and remove it
in time for matplotlib 3.13.
"""
def get_transform(self):
"""
Return the `.Transform` object associated with this scale.
"""
raise NotImplementedError()
def set_default_locators_and_formatters(self, axis):
"""
Set the locators and formatters of *axis* to instances suitable for
this scale.
"""
raise NotImplementedError()
def limit_range_for_scale(self, vmin, vmax, minpos):
"""
Return the range *vmin*, *vmax*, restricted to the
domain supported by this scale (if any).
*minpos* should be the minimum positive value in the data.
This is used by log scales to determine a minimum value.
"""
return vmin, vmax
def val_in_range(self, val):
"""
Return whether the value(s) are within the valid range for this scale.
Accepts a scalar or array-like ``val``. For a scalar, returns a
Python ``bool``. For an array, returns a bool ndarray of the same
shape. This is a generic implementation, and subclasses may implement
more efficient solutions for their domain.
"""
arr = np.asarray(val)
with np.errstate(invalid='ignore'):
try:
vmin, vmax = self.limit_range_for_scale(arr, arr, minpos=1e-300)
except (TypeError, ValueError):
result = np.zeros(arr.shape, dtype=bool)
else:
result = np.isfinite(arr) & (vmin == arr) & (vmax == arr)
return bool(result) if arr.ndim == 0 else result
def _make_axis_parameter_optional(init_func):
"""
Decorator to allow leaving out the *axis* parameter in scale constructors.
This decorator ensures backward compatibility for scale classes that
previously required an *axis* parameter. It allows constructors to be
called with or without the *axis* parameter.
For simplicity, this does not handle the case when *axis*
is passed as a keyword. However,
scanning GitHub, there's no evidence that that is used anywhere.
Parameters
----------
init_func : callable
The original __init__ method of a scale class.
Returns
-------
callable
A wrapped version of *init_func* that handles the optional *axis*.
Notes
-----
If the wrapped constructor defines *axis* as its first argument, the
parameter is preserved when present. Otherwise, the value `None` is injected
as the first argument.
Examples
--------
>>> from matplotlib.scale import ScaleBase
>>> class CustomScale(ScaleBase):
... @_make_axis_parameter_optional
... def __init__(self, axis, custom_param=1):
... self.custom_param = custom_param
"""
@wraps(init_func)
def wrapper(self, *args, **kwargs):
sig = inspect.signature(init_func)
try:
# Try old signature.
sig.bind(self, *args, **kwargs)
except TypeError:
# Use the new signature and pass in an unused axis=None.
init_func(self, None, *args, **kwargs)
else:
# Use the old signature.
init_func(self, *args, **kwargs)
return wrapper
class LinearScale(ScaleBase):
"""
The default linear scale.
"""
name = 'linear'
@_make_axis_parameter_optional
def __init__(self, axis):
# This method is present only to prevent inheritance of the base class'
# constructor docstring, which would otherwise end up interpolated into
# the docstring of Axis.set_scale.
"""
""" # noqa: D419
def set_default_locators_and_formatters(self, axis):
# docstring inherited
axis.set_major_locator(AutoLocator())
axis.set_major_formatter(ScalarFormatter())
axis.set_minor_formatter(NullFormatter())
# update the minor locator for x and y axis based on rcParams
if (axis.axis_name == 'x' and mpl.rcParams['xtick.minor.visible'] or
axis.axis_name == 'y' and mpl.rcParams['ytick.minor.visible']):
axis.set_minor_locator(AutoMinorLocator())
else:
axis.set_minor_locator(NullLocator())
def get_transform(self):
"""
Return the transform for linear scaling, which is just the
`~matplotlib.transforms.IdentityTransform`.
"""
return IdentityTransform()
def val_in_range(self, val):
"""
Return whether the value(s) are within the valid range for this scale.
This is True for all values, except +-inf and NaN.
"""
arr = np.asarray(val)
result = np.isfinite(arr)
return bool(result) if arr.ndim == 0 else result
class FuncTransform(Transform):
"""
A simple transform that takes and arbitrary function for the
forward and inverse transform.
"""
input_dims = output_dims = 1
def __init__(self, forward, inverse):
"""
Parameters
----------
forward : callable
The forward function for the transform. This function must have
an inverse and, for best behavior, be monotonic.
It must have the signature::
def forward(values: array-like) -> array-like
inverse : callable
The inverse of the forward function. Signature as ``forward``.
"""
super().__init__()
if callable(forward) and callable(inverse):
self._forward = forward
self._inverse = inverse
else:
raise ValueError('arguments to FuncTransform must be functions')
def transform_non_affine(self, values):
return self._forward(values)
def inverted(self):
return FuncTransform(self._inverse, self._forward)
class FuncScale(ScaleBase):
"""
Provide an arbitrary scale with user-supplied function for the axis.
"""
name = 'function'
@_make_axis_parameter_optional
def __init__(self, axis, functions):
"""
Parameters
----------
axis : `~matplotlib.axis.Axis`
The axis for the scale.
.. note::
This parameter is unused and will be removed in an imminent release.
It can already be left out because of special preprocessing,
so that ``FuncScale(functions)`` is valid.
functions : (callable, callable)
two-tuple of the forward and inverse functions for the scale.
The forward function must be monotonic.
Both functions must have the signature::
def forward(values: array-like) -> array-like
"""
forward, inverse = functions
transform = FuncTransform(forward, inverse)
self._transform = transform
def get_transform(self):
"""Return the `.FuncTransform` associated with this scale."""
return self._transform
def set_default_locators_and_formatters(self, axis):
# docstring inherited
axis.set_major_locator(AutoLocator())
axis.set_major_formatter(ScalarFormatter())
axis.set_minor_formatter(NullFormatter())
# update the minor locator for x and y axis based on rcParams
if (axis.axis_name == 'x' and mpl.rcParams['xtick.minor.visible'] or
axis.axis_name == 'y' and mpl.rcParams['ytick.minor.visible']):
axis.set_minor_locator(AutoMinorLocator())
else:
axis.set_minor_locator(NullLocator())
class LogTransform(Transform):
input_dims = output_dims = 1
def __init__(self, base, nonpositive='clip'):
super().__init__()
if base