123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688 |
- """
- Scales define the distribution of data values on an axis, e.g. a log scaling.
- They are attached to an `~.axis.Axis` and hold a `.Transform`, which is
- responsible for the actual data transformation.
- See also `.axes.Axes.set_xscale` and the scales examples in the documentation.
- """
- import inspect
- import textwrap
- import numpy as np
- from numpy import ma
- import matplotlib as mpl
- from matplotlib import cbook, docstring
- from matplotlib.ticker import (
- NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
- NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
- SymmetricalLogLocator, LogitLocator)
- from matplotlib.transforms import Transform, IdentityTransform
- from matplotlib.cbook import warn_deprecated
- class ScaleBase:
- """
- The base class for all scales.
- Scales are separable transformations, working on a single dimension.
- Any subclasses will want to override:
- - :attr:`name`
- - :meth:`get_transform`
- - :meth:`set_default_locators_and_formatters`
- And optionally:
- - :meth:`limit_range_for_scale`
- """
- def __init__(self, axis, **kwargs):
- 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.
- """
- if kwargs:
- warn_deprecated(
- '3.2', removal='3.4',
- message=(
- f"ScaleBase got an unexpected keyword argument "
- f"{next(iter(kwargs))!r}. This will become an error "
- "%(removal)s.")
- )
- def get_transform(self):
- """
- Return the :class:`~matplotlib.transforms.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, **kwargs):
- # 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.
- """
- """
- super().__init__(axis, **kwargs)
- 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
- @cbook._rename_parameter("3.3", "nonpos", "nonpositive")
- def __init__(self, base, nonpositive='clip'):
- Transform.__init__(self)
- if base <= 0 or base == 1:
- raise ValueError('The log base cannot be <= 0 or == 1')
- self.base = base
- self._clip = cbook._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")
- def transform_non_affine(self, a):
- # 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(a)
- else:
- out = np.log(a)
- 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[a <= 0] = -1000
- return out
- def inverted(self):
- return InvertedLogTransform(self.base)
- class InvertedLogTransform(Transform):
- input_dims = output_dims = 1
- def __init__(self, base):
- Transform.__init__(self)
- self.base = base
- def __str__(self):
- return "{}(base={})".format(type(self).__name__, self.base)
- def transform_non_affine(self, a):
- return ma.power(self.base, a)
- def inverted(self):
- return LogTransform(self.base)
- class LogScale(ScaleBase):
- """
- A standard logarithmic scale. Care is taken to only plot positive values.
- """
- name = 'log'
- @cbook.deprecated("3.3", alternative="scale.LogTransform")
- @property
- def LogTransform(self):
- return LogTransform
- @cbook.deprecated("3.3", alternative="scale.InvertedLogTransform")
- @property
- def InvertedLogTransform(self):
- return InvertedLogTransform
- def __init__(self, axis, **kwargs):
- """
- 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.
- """
- # After the deprecation, the whole (outer) __init__ can be replaced by
- # def __init__(self, axis, *, base=10, subs=None, nonpositive="clip")
- # The following is to emit the right warnings depending on the axis
- # used, as the *old* kwarg names depended on the axis.
- axis_name = getattr(axis, "axis_name", "x")
- @cbook._rename_parameter("3.3", f"base{axis_name}", "base")
- @cbook._rename_parameter("3.3", f"subs{axis_name}", "subs")
- @cbook._rename_parameter("3.3", f"nonpos{axis_name}", "nonpositive")
- def __init__(*, base=10, subs=None, nonpositive="clip"):
- return base, subs, nonpositive
- base, subs, nonpositive = __init__(**kwargs)
- 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):
- Transform.__init__(self)
- 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)
- def transform_non_affine(self, a):
- abs_a = np.abs(a)
- with np.errstate(divide="ignore", invalid="ignore"):
- out = np.sign(a) * self.linthresh * (
- self._linscale_adj +
- np.log(abs_a / self.linthresh) / self._log_base)
- inside = abs_a <= self.linthresh
- out[inside] = a[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):
- Transform.__init__(self)
- 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))
- def transform_non_affine(self, a):
- abs_a = np.abs(a)
- with np.errstate(divide="ignore", invalid="ignore"):
- out = np.sign(a) * self.linthresh * (
- np.power(self.base,
- abs_a / self.linthresh - self._linscale_adj))
- inside = abs_a <= self.invlinthresh
- out[inside] = a[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'
- @cbook.deprecated("3.3", alternative="scale.SymmetricalLogTransform")
- @property
- def SymmetricalLogTransform(self):
- return SymmetricalLogTransform
- @cbook.deprecated(
- "3.3", alternative="scale.InvertedSymmetricalLogTransform")
- @property
- def InvertedSymmetricalLogTransform(self):
- return InvertedSymmetricalLogTransform
- def __init__(self, axis, **kwargs):
- axis_name = getattr(axis, "axis_name", "x")
- # See explanation in LogScale.__init__.
- @cbook._rename_parameter("3.3", f"base{axis_name}", "base")
- @cbook._rename_parameter("3.3", f"linthresh{axis_name}", "linthresh")
- @cbook._rename_parameter("3.3", f"subs{axis_name}", "subs")
- @cbook._rename_parameter("3.3", f"linscale{axis_name}", "linscale")
- def __init__(*, base=10, linthresh=2, subs=None, linscale=1, **kwargs):
- if kwargs:
- warn_deprecated(
- '3.2', removal='3.4',
- message=(
- f"SymmetricalLogScale got an unexpected keyword "
- f"argument {next(iter(kwargs))!r}. This will become "
- "an error %(removal)s.")
- )
- return base, linthresh, subs, linscale
- base, linthresh, subs, linscale = __init__(**kwargs)
- 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 LogitTransform(Transform):
- input_dims = output_dims = 1
- @cbook._rename_parameter("3.3", "nonpos", "nonpositive")
- def __init__(self, nonpositive='mask'):
- Transform.__init__(self)
- cbook._check_in_list(['mask', 'clip'], nonpositive=nonpositive)
- self._nonpositive = nonpositive
- self._clip = {"clip": True, "mask": False}[nonpositive]
- def transform_non_affine(self, a):
- """logit transform (base 10), masked or clipped"""
- with np.errstate(divide="ignore", invalid="ignore"):
- out = np.log10(a / (1 - a))
- if self._clip: # See LogTransform for choice of clip value.
- out[a <= 0] = -1000
- out[1 <= a] = 1000
- return out
- def inverted(self):
- return LogisticTransform(self._nonpositive)
- def __str__(self):
- return "{}({!r})".format(type(self).__name__, self._nonpositive)
- class LogisticTransform(Transform):
- input_dims = output_dims = 1
- @cbook._rename_parameter("3.3", "nonpos", "nonpositive")
- def __init__(self, nonpositive='mask'):
- Transform.__init__(self)
- self._nonpositive = nonpositive
- def transform_non_affine(self, a):
- """logistic transform (base 10)"""
- return 1.0 / (1 + 10**(-a))
- def inverted(self):
- return LogitTransform(self._nonpositive)
- def __str__(self):
- return "{}({!r})".format(type(self).__name__, self._nonpositive)
- 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'
- @cbook._rename_parameter("3.3", "nonpos", "nonpositive")
- 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,
- '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 = scale.lower()
- cbook._check_in_list(_scale_mapping, scale=scale)
- return _scale_mapping[scale](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():
- docs.extend([
- f" {name!r}",
- "",
- textwrap.indent(inspect.getdoc(scale_class.__init__), " " * 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(),
- )
|