123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756 |
- """
- Scales define the distribution of data values on an axis, e.g. a log scaling.
- They are defined as subclasses of `ScaleBase`.
- See also `.axes.Axes.set_xscale` and the scales examples in the documentation.
- See :doc:`/gallery/scales/custom_scale` for a full example of defining a custom
- scale.
- Matplotlib also supports non-separable transformations that operate on both
- `~.axis.Axis` at the same time. They are known as projections, and defined in
- `matplotlib.projections`.
- """
- import inspect
- import textwrap
- 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 implementors.
- For back-compatibility reasons, scales take an `~matplotlib.axis.Axis`
- object as first argument. However, this argument should not
- be used: a single scale object should be usable by multiple
- `~matplotlib.axis.Axis`\es at the same time.
- """
- 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
- class LinearScale(ScaleBase):
- """
- The default linear scale.
- """
- name = 'linear'
- 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()
- 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'
- def __init__(self, axis, functions):
- """
- Parameters
- ----------
- axis : `~matplotlib.axis.Axis`
- The axis for the scale.
- 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 <= 0 or base == 1:
- raise ValueError('The log base cannot be <= 0 or == 1')
- self.base = base
- self._clip = _api.check_getitem(
- {"clip": True, "mask": False}, nonpositive=nonpositive)
- def __str__(self):
- return "{}(base={}, nonpositive={!r})".format(
- type(self).__name__, self.base, "clip" if self._clip else "mask")
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- # Ignore invalid values due to nans being passed to the transform.
- with np.errstate(divide="ignore", invalid="ignore"):
- log = {np.e: np.log, 2: np.log2, 10: np.log10}.get(self.base)
- if log: # If possible, do everything in a single call to NumPy.
- out = log(values)
- else:
- out = np.log(values)
- out /= np.log(self.base)
- if self._clip:
- # SVG spec says that conforming viewers must support values up
- # to 3.4e38 (C float); however experiments suggest that
- # Inkscape (which uses cairo for rendering) runs into cairo's
- # 24-bit limit (which is apparently shared by Agg).
- # Ghostscript (used for pdf rendering appears to overflow even
- # earlier, with the max value around 2 ** 15 for the tests to
- # pass. On the other hand, in practice, we want to clip beyond
- # np.log10(np.nextafter(0, 1)) ~ -323
- # so 1000 seems safe.
- out[values <= 0] = -1000
- return out
- def inverted(self):
- return InvertedLogTransform(self.base)
- class InvertedLogTransform(Transform):
- input_dims = output_dims = 1
- def __init__(self, base):
- super().__init__()
- self.base = base
- def __str__(self):
- return f"{type(self).__name__}(base={self.base})"
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- return np.power(self.base, values)
- def inverted(self):
- return LogTransform(self.base)
- class LogScale(ScaleBase):
- """
- A standard logarithmic scale. Care is taken to only plot positive values.
- """
- name = 'log'
- def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"):
- """
- Parameters
- ----------
- axis : `~matplotlib.axis.Axis`
- The axis for the scale.
- base : float, default: 10
- The base of the logarithm.
- nonpositive : {'clip', 'mask'}, default: 'clip'
- Determines the behavior for non-positive values. They can either
- be masked as invalid, or clipped to a very small positive number.
- subs : sequence of int, default: None
- Where to place the subticks between each major tick. For example,
- in a log10 scale, ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place 8
- logarithmically spaced minor ticks between each major tick.
- """
- self._transform = LogTransform(base, nonpositive)
- self.subs = subs
- base = property(lambda self: self._transform.base)
- def set_default_locators_and_formatters(self, axis):
- # docstring inherited
- axis.set_major_locator(LogLocator(self.base))
- axis.set_major_formatter(LogFormatterSciNotation(self.base))
- axis.set_minor_locator(LogLocator(self.base, self.subs))
- axis.set_minor_formatter(
- LogFormatterSciNotation(self.base,
- labelOnlyBase=(self.subs is not None)))
- def get_transform(self):
- """Return the `.LogTransform` associated with this scale."""
- return self._transform
- def limit_range_for_scale(self, vmin, vmax, minpos):
- """Limit the domain to positive values."""
- if not np.isfinite(minpos):
- minpos = 1e-300 # Should rarely (if ever) have a visible effect.
- return (minpos if vmin <= 0 else vmin,
- minpos if vmax <= 0 else vmax)
- class FuncScaleLog(LogScale):
- """
- Provide an arbitrary scale with user-supplied function for the axis and
- then put on a logarithmic axes.
- """
- name = 'functionlog'
- def __init__(self, axis, functions, base=10):
- """
- Parameters
- ----------
- axis : `~matplotlib.axis.Axis`
- The axis for the scale.
- 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
- base : float, default: 10
- Logarithmic base of the scale.
- """
- forward, inverse = functions
- self.subs = None
- self._transform = FuncTransform(forward, inverse) + LogTransform(base)
- @property
- def base(self):
- return self._transform._b.base # Base of the LogTransform.
- def get_transform(self):
- """Return the `.Transform` associated with this scale."""
- return self._transform
- class SymmetricalLogTransform(Transform):
- input_dims = output_dims = 1
- def __init__(self, base, linthresh, linscale):
- super().__init__()
- if base <= 1.0:
- raise ValueError("'base' must be larger than 1")
- if linthresh <= 0.0:
- raise ValueError("'linthresh' must be positive")
- if linscale <= 0.0:
- raise ValueError("'linscale' must be positive")
- self.base = base
- self.linthresh = linthresh
- self.linscale = linscale
- self._linscale_adj = (linscale / (1.0 - self.base ** -1))
- self._log_base = np.log(base)
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- abs_a = np.abs(values)
- with np.errstate(divide="ignore", invalid="ignore"):
- out = np.sign(values) * self.linthresh * (
- self._linscale_adj +
- np.log(abs_a / self.linthresh) / self._log_base)
- inside = abs_a <= self.linthresh
- out[inside] = values[inside] * self._linscale_adj
- return out
- def inverted(self):
- return InvertedSymmetricalLogTransform(self.base, self.linthresh,
- self.linscale)
- class InvertedSymmetricalLogTransform(Transform):
- input_dims = output_dims = 1
- def __init__(self, base, linthresh, linscale):
- super().__init__()
- symlog = SymmetricalLogTransform(base, linthresh, linscale)
- self.base = base
- self.linthresh = linthresh
- self.invlinthresh = symlog.transform(linthresh)
- self.linscale = linscale
- self._linscale_adj = (linscale / (1.0 - self.base ** -1))
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- abs_a = np.abs(values)
- with np.errstate(divide="ignore", invalid="ignore"):
- out = np.sign(values) * self.linthresh * (
- np.power(self.base,
- abs_a / self.linthresh - self._linscale_adj))
- inside = abs_a <= self.invlinthresh
- out[inside] = values[inside] / self._linscale_adj
- return out
- def inverted(self):
- return SymmetricalLogTransform(self.base,
- self.linthresh, self.linscale)
- class SymmetricalLogScale(ScaleBase):
- """
- The symmetrical logarithmic scale is logarithmic in both the
- positive and negative directions from the origin.
- Since the values close to zero tend toward infinity, there is a
- need to have a range around zero that is linear. The parameter
- *linthresh* allows the user to specify the size of this range
- (-*linthresh*, *linthresh*).
- Parameters
- ----------
- base : float, default: 10
- The base of the logarithm.
- linthresh : float, default: 2
- Defines the range ``(-x, x)``, within which the plot is linear.
- This avoids having the plot go to infinity around zero.
- subs : sequence of int
- Where to place the subticks between each major tick.
- For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place
- 8 logarithmically spaced minor ticks between each major tick.
- linscale : float, optional
- This allows the linear range ``(-linthresh, linthresh)`` to be
- stretched relative to the logarithmic range. Its value is the number of
- decades to use for each half of the linear range. For example, when
- *linscale* == 1.0 (the default), the space used for the positive and
- negative halves of the linear range will be equal to one decade in
- the logarithmic range.
- """
- name = 'symlog'
- def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1):
- self._transform = SymmetricalLogTransform(base, linthresh, linscale)
- self.subs = subs
- base = property(lambda self: self._transform.base)
- linthresh = property(lambda self: self._transform.linthresh)
- linscale = property(lambda self: self._transform.linscale)
- def set_default_locators_and_formatters(self, axis):
- # docstring inherited
- axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
- axis.set_major_formatter(LogFormatterSciNotation(self.base))
- axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
- self.subs))
- axis.set_minor_formatter(NullFormatter())
- def get_transform(self):
- """Return the `.SymmetricalLogTransform` associated with this scale."""
- return self._transform
- class AsinhTransform(Transform):
- """Inverse hyperbolic-sine transformation used by `.AsinhScale`"""
- input_dims = output_dims = 1
- def __init__(self, linear_width):
- super().__init__()
- if linear_width <= 0.0:
- raise ValueError("Scale parameter 'linear_width' " +
- "must be strictly positive")
- self.linear_width = linear_width
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- return self.linear_width * np.arcsinh(values / self.linear_width)
- def inverted(self):
- return InvertedAsinhTransform(self.linear_width)
- class InvertedAsinhTransform(Transform):
- """Hyperbolic sine transformation used by `.AsinhScale`"""
- input_dims = output_dims = 1
- def __init__(self, linear_width):
- super().__init__()
- self.linear_width = linear_width
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- return self.linear_width * np.sinh(values / self.linear_width)
- def inverted(self):
- return AsinhTransform(self.linear_width)
- class AsinhScale(ScaleBase):
- """
- A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh)
- For values close to zero, this is essentially a linear scale,
- but for large magnitude values (either positive or negative)
- it is asymptotically logarithmic. The transition between these
- linear and logarithmic regimes is smooth, and has no discontinuities
- in the function gradient in contrast to
- the `.SymmetricalLogScale` ("symlog") scale.
- Specifically, the transformation of an axis coordinate :math:`a` is
- :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0`
- is the effective width of the linear region of the transformation.
- In that region, the transformation is
- :math:`a \\rightarrow a + \\mathcal{O}(a^3)`.
- For large values of :math:`a` the transformation behaves as
- :math:`a \\rightarrow a_0 \\, \\mathrm{sgn}(a) \\ln |a| + \\mathcal{O}(1)`.
- .. note::
- This API is provisional and may be revised in the future
- based on early user feedback.
- """
- name = 'asinh'
- auto_tick_multipliers = {
- 3: (2, ),
- 4: (2, ),
- 5: (2, ),
- 8: (2, 4),
- 10: (2, 5),
- 16: (2, 4, 8),
- 64: (4, 16),
- 1024: (256, 512)
- }
- def __init__(self, axis, *, linear_width=1.0,
- base=10, subs='auto', **kwargs):
- """
- Parameters
- ----------
- linear_width : float, default: 1
- The scale parameter (elsewhere referred to as :math:`a_0`)
- defining the extent of the quasi-linear region,
- and the coordinate values beyond which the transformation
- becomes asymptotically logarithmic.
- base : int, default: 10
- The number base used for rounding tick locations
- on a logarithmic scale. If this is less than one,
- then rounding is to the nearest integer multiple
- of powers of ten.
- subs : sequence of int
- Multiples of the number base used for minor ticks.
- If set to 'auto', this will use built-in defaults,
- e.g. (2, 5) for base=10.
- """
- super().__init__(axis)
- self._transform = AsinhTransform(linear_width)
- self._base = int(base)
- if subs == 'auto':
- self._subs = self.auto_tick_multipliers.get(self._base)
- else:
- self._subs = subs
- linear_width = property(lambda self: self._transform.linear_width)
- def get_transform(self):
- return self._transform
- def set_default_locators_and_formatters(self, axis):
- axis.set(major_locator=AsinhLocator(self.linear_width,
- base=self._base),
- minor_locator=AsinhLocator(self.linear_width,
- base=self._base,
- subs=self._subs),
- minor_formatter=NullFormatter())
- if self._base > 1:
- axis.set_major_formatter(LogFormatterSciNotation(self._base))
- else:
- axis.set_major_formatter('{x:.3g}')
- class LogitTransform(Transform):
- input_dims = output_dims = 1
- def __init__(self, nonpositive='mask'):
- super().__init__()
- _api.check_in_list(['mask', 'clip'], nonpositive=nonpositive)
- self._nonpositive = nonpositive
- self._clip = {"clip": True, "mask": False}[nonpositive]
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- """logit transform (base 10), masked or clipped"""
- with np.errstate(divide="ignore", invalid="ignore"):
- out = np.log10(values / (1 - values))
- if self._clip: # See LogTransform for choice of clip value.
- out[values <= 0] = -1000
- out[1 <= values] = 1000
- return out
- def inverted(self):
- return LogisticTransform(self._nonpositive)
- def __str__(self):
- return f"{type(self).__name__}({self._nonpositive!r})"
- class LogisticTransform(Transform):
- input_dims = output_dims = 1
- def __init__(self, nonpositive='mask'):
- super().__init__()
- self._nonpositive = nonpositive
- @_api.rename_parameter("3.8", "a", "values")
- def transform_non_affine(self, values):
- """logistic transform (base 10)"""
- return 1.0 / (1 + 10**(-values))
- def inverted(self):
- return LogitTransform(self._nonpositive)
- def __str__(self):
- return f"{type(self).__name__}({self._nonpositive!r})"
- class LogitScale(ScaleBase):
- """
- Logit scale for data between zero and one, both excluded.
- This scale is similar to a log scale close to zero and to one, and almost
- linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
- """
- name = 'logit'
- def __init__(self, axis, nonpositive='mask', *,
- one_half=r"\frac{1}{2}", use_overline=False):
- r"""
- Parameters
- ----------
- axis : `~matplotlib.axis.Axis`
- Currently unused.
- nonpositive : {'mask', 'clip'}
- Determines the behavior for values beyond the open interval ]0, 1[.
- They can either be masked as invalid, or clipped to a number very
- close to 0 or 1.
- use_overline : bool, default: False
- Indicate the usage of survival notation (\overline{x}) in place of
- standard notation (1-x) for probability close to one.
- one_half : str, default: r"\frac{1}{2}"
- The string used for ticks formatter to represent 1/2.
- """
- self._transform = LogitTransform(nonpositive)
- self._use_overline = use_overline
- self._one_half = one_half
- def get_transform(self):
- """Return the `.LogitTransform` associated with this scale."""
- return self._transform
- def set_default_locators_and_formatters(self, axis):
- # docstring inherited
- # ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
- axis.set_major_locator(LogitLocator())
- axis.set_major_formatter(
- LogitFormatter(
- one_half=self._one_half,
- use_overline=self._use_overline
- )
- )
- axis.set_minor_locator(LogitLocator(minor=True))
- axis.set_minor_formatter(
- LogitFormatter(
- minor=True,
- one_half=self._one_half,
- use_overline=self._use_overline
- )
- )
- def limit_range_for_scale(self, vmin, vmax, minpos):
- """
- Limit the domain to values between 0 and 1 (excluded).
- """
- if not np.isfinite(minpos):
- minpos = 1e-7 # Should rarely (if ever) have a visible effect.
- return (minpos if vmin <= 0 else vmin,
- 1 - minpos if vmax >= 1 else vmax)
- _scale_mapping = {
- 'linear': LinearScale,
- 'log': LogScale,
- 'symlog': SymmetricalLogScale,
- 'asinh': AsinhScale,
- 'logit': LogitScale,
- 'function': FuncScale,
- 'functionlog': FuncScaleLog,
- }
- def get_scale_names():
- """Return the names of the available scales."""
- return sorted(_scale_mapping)
- def scale_factory(scale, axis, **kwargs):
- """
- Return a scale class by name.
- Parameters
- ----------
- scale : {%(names)s}
- axis : `~matplotlib.axis.Axis`
- """
- scale_cls = _api.check_getitem(_scale_mapping, scale=scale)
- return scale_cls(axis, **kwargs)
- if scale_factory.__doc__:
- scale_factory.__doc__ = scale_factory.__doc__ % {
- "names": ", ".join(map(repr, get_scale_names()))}
- def register_scale(scale_class):
- """
- Register a new kind of scale.
- Parameters
- ----------
- scale_class : subclass of `ScaleBase`
- The scale to register.
- """
- _scale_mapping[scale_class.name] = scale_class
- def _get_scale_docs():
- """
- Helper function for generating docstrings related to scales.
- """
- docs = []
- for name, scale_class in _scale_mapping.items():
- docstring = inspect.getdoc(scale_class.__init__) or ""
- docs.extend([
- f" {name!r}",
- "",
- textwrap.indent(docstring, " " * 8),
- ""
- ])
- return "\n".join(docs)
- _docstring.interpd.update(
- scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]),
- scale_docs=_get_scale_docs().rstrip(),
- )
|