Coverage for /usr/lib/python3/dist-packages/matplotlib/scale.py: 56%
277 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2Scales define the distribution of data values on an axis, e.g. a log scaling.
3They are defined as subclasses of `ScaleBase`.
5See also `.axes.Axes.set_xscale` and the scales examples in the documentation.
7See :doc:`/gallery/scales/custom_scale` for a full example of defining a custom
8scale.
10Matplotlib also supports non-separable transformations that operate on both
11`~.axis.Axis` at the same time. They are known as projections, and defined in
12`matplotlib.projections`.
13"""
15import inspect
16import textwrap
18import numpy as np
20import matplotlib as mpl
21from matplotlib import _api, _docstring
22from matplotlib.ticker import (
23 NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
24 NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
25 SymmetricalLogLocator, AsinhLocator, LogitLocator)
26from matplotlib.transforms import Transform, IdentityTransform
29class ScaleBase:
30 """
31 The base class for all scales.
33 Scales are separable transformations, working on a single dimension.
35 Subclasses should override
37 :attr:`name`
38 The scale's name.
39 :meth:`get_transform`
40 A method returning a `.Transform`, which converts data coordinates to
41 scaled coordinates. This transform should be invertible, so that e.g.
42 mouse positions can be converted back to data coordinates.
43 :meth:`set_default_locators_and_formatters`
44 A method that sets default locators and formatters for an `~.axis.Axis`
45 that uses this scale.
46 :meth:`limit_range_for_scale`
47 An optional method that "fixes" the axis range to acceptable values,
48 e.g. restricting log-scaled axes to positive values.
49 """
51 def __init__(self, axis):
52 r"""
53 Construct a new scale.
55 Notes
56 -----
57 The following note is for scale implementors.
59 For back-compatibility reasons, scales take an `~matplotlib.axis.Axis`
60 object as first argument. However, this argument should not
61 be used: a single scale object should be usable by multiple
62 `~matplotlib.axis.Axis`\es at the same time.
63 """
65 def get_transform(self):
66 """
67 Return the `.Transform` object associated with this scale.
68 """
69 raise NotImplementedError()
71 def set_default_locators_and_formatters(self, axis):
72 """
73 Set the locators and formatters of *axis* to instances suitable for
74 this scale.
75 """
76 raise NotImplementedError()
78 def limit_range_for_scale(self, vmin, vmax, minpos):
79 """
80 Return the range *vmin*, *vmax*, restricted to the
81 domain supported by this scale (if any).
83 *minpos* should be the minimum positive value in the data.
84 This is used by log scales to determine a minimum value.
85 """
86 return vmin, vmax
89class LinearScale(ScaleBase):
90 """
91 The default linear scale.
92 """
94 name = 'linear'
96 def __init__(self, axis):
97 # This method is present only to prevent inheritance of the base class'
98 # constructor docstring, which would otherwise end up interpolated into
99 # the docstring of Axis.set_scale.
100 """
101 """
103 def set_default_locators_and_formatters(self, axis):
104 # docstring inherited
105 axis.set_major_locator(AutoLocator())
106 axis.set_major_formatter(ScalarFormatter())
107 axis.set_minor_formatter(NullFormatter())
108 # update the minor locator for x and y axis based on rcParams
109 if (axis.axis_name == 'x' and mpl.rcParams['xtick.minor.visible'] or
110 axis.axis_name == 'y' and mpl.rcParams['ytick.minor.visible']):
111 axis.set_minor_locator(AutoMinorLocator())
112 else:
113 axis.set_minor_locator(NullLocator())
115 def get_transform(self):
116 """
117 Return the transform for linear scaling, which is just the
118 `~matplotlib.transforms.IdentityTransform`.
119 """
120 return IdentityTransform()
123class FuncTransform(Transform):
124 """
125 A simple transform that takes and arbitrary function for the
126 forward and inverse transform.
127 """
129 input_dims = output_dims = 1
131 def __init__(self, forward, inverse):
132 """
133 Parameters
134 ----------
135 forward : callable
136 The forward function for the transform. This function must have
137 an inverse and, for best behavior, be monotonic.
138 It must have the signature::
140 def forward(values: array-like) -> array-like
142 inverse : callable
143 The inverse of the forward function. Signature as ``forward``.
144 """
145 super().__init__()
146 if callable(forward) and callable(inverse):
147 self._forward = forward
148 self._inverse = inverse
149 else:
150 raise ValueError('arguments to FuncTransform must be functions')
152 def transform_non_affine(self, values):
153 return self._forward(values)
155 def inverted(self):
156 return FuncTransform(self._inverse, self._forward)
159class FuncScale(ScaleBase):
160 """
161 Provide an arbitrary scale with user-supplied function for the axis.
162 """
164 name = 'function'
166 def __init__(self, axis, functions):
167 """
168 Parameters
169 ----------
170 axis : `~matplotlib.axis.Axis`
171 The axis for the scale.
172 functions : (callable, callable)
173 two-tuple of the forward and inverse functions for the scale.
174 The forward function must be monotonic.
176 Both functions must have the signature::
178 def forward(values: array-like) -> array-like
179 """
180 forward, inverse = functions
181 transform = FuncTransform(forward, inverse)
182 self._transform = transform
184 def get_transform(self):
185 """Return the `.FuncTransform` associated with this scale."""
186 return self._transform
188 def set_default_locators_and_formatters(self, axis):
189 # docstring inherited
190 axis.set_major_locator(AutoLocator())
191 axis.set_major_formatter(ScalarFormatter())
192 axis.set_minor_formatter(NullFormatter())
193 # update the minor locator for x and y axis based on rcParams
194 if (axis.axis_name == 'x' and mpl.rcParams['xtick.minor.visible'] or
195 axis.axis_name == 'y' and mpl.rcParams['ytick.minor.visible']):
196 axis.set_minor_locator(AutoMinorLocator())
197 else:
198 axis.set_minor_locator(NullLocator())
201class LogTransform(Transform):
202 input_dims = output_dims = 1
204 def __init__(self, base, nonpositive='clip'):
205 super().__init__()
206 if base <= 0 or base == 1:
207 raise ValueError('The log base cannot be <= 0 or == 1')
208 self.base = base
209 self._clip = _api.check_getitem(
210 {"clip": True, "mask": False}, nonpositive=nonpositive)
212 def __str__(self):
213 return "{}(base={}, nonpositive={!r})".format(
214 type(self).__name__, self.base, "clip" if self._clip else "mask")
216 def transform_non_affine(self, a):
217 # Ignore invalid values due to nans being passed to the transform.
218 with np.errstate(divide="ignore", invalid="ignore"):
219 log = {np.e: np.log, 2: np.log2, 10: np.log10}.get(self.base)
220 if log: # If possible, do everything in a single call to NumPy.
221 out = log(a)
222 else:
223 out = np.log(a)
224 out /= np.log(self.base)
225 if self._clip:
226 # SVG spec says that conforming viewers must support values up
227 # to 3.4e38 (C float); however experiments suggest that
228 # Inkscape (which uses cairo for rendering) runs into cairo's
229 # 24-bit limit (which is apparently shared by Agg).
230 # Ghostscript (used for pdf rendering appears to overflow even
231 # earlier, with the max value around 2 ** 15 for the tests to
232 # pass. On the other hand, in practice, we want to clip beyond
233 # np.log10(np.nextafter(0, 1)) ~ -323
234 # so 1000 seems safe.
235 out[a <= 0] = -1000
236 return out
238 def inverted(self):
239 return InvertedLogTransform(self.base)
242class InvertedLogTransform(Transform):
243 input_dims = output_dims = 1
245 def __init__(self, base):
246 super().__init__()
247 self.base = base
249 def __str__(self):
250 return "{}(base={})".format(type(self).__name__, self.base)
252 def transform_non_affine(self, a):
253 return np.power(self.base, a)
255 def inverted(self):
256 return LogTransform(self.base)
259class LogScale(ScaleBase):
260 """
261 A standard logarithmic scale. Care is taken to only plot positive values.
262 """
263 name = 'log'
265 def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"):
266 """
267 Parameters
268 ----------
269 axis : `~matplotlib.axis.Axis`
270 The axis for the scale.
271 base : float, default: 10
272 The base of the logarithm.
273 nonpositive : {'clip', 'mask'}, default: 'clip'
274 Determines the behavior for non-positive values. They can either
275 be masked as invalid, or clipped to a very small positive number.
276 subs : sequence of int, default: None
277 Where to place the subticks between each major tick. For example,
278 in a log10 scale, ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place 8
279 logarithmically spaced minor ticks between each major tick.
280 """
281 self._transform = LogTransform(base, nonpositive)
282 self.subs = subs
284 base = property(lambda self: self._transform.base)
286 def set_default_locators_and_formatters(self, axis):
287 # docstring inherited
288 axis.set_major_locator(LogLocator(self.base))
289 axis.set_major_formatter(LogFormatterSciNotation(self.base))
290 axis.set_minor_locator(LogLocator(self.base, self.subs))
291 axis.set_minor_formatter(
292 LogFormatterSciNotation(self.base,
293 labelOnlyBase=(self.subs is not None)))
295 def get_transform(self):
296 """Return the `.LogTransform` associated with this scale."""
297 return self._transform
299 def limit_range_for_scale(self, vmin, vmax, minpos):
300 """Limit the domain to positive values."""
301 if not np.isfinite(minpos):
302 minpos = 1e-300 # Should rarely (if ever) have a visible effect.
304 return (minpos if vmin <= 0 else vmin,
305 minpos if vmax <= 0 else vmax)
308class FuncScaleLog(LogScale):
309 """
310 Provide an arbitrary scale with user-supplied function for the axis and
311 then put on a logarithmic axes.
312 """
314 name = 'functionlog'
316 def __init__(self, axis, functions, base=10):
317 """
318 Parameters
319 ----------
320 axis : `matplotlib.axis.Axis`
321 The axis for the scale.
322 functions : (callable, callable)
323 two-tuple of the forward and inverse functions for the scale.
324 The forward function must be monotonic.
326 Both functions must have the signature::
328 def forward(values: array-like) -> array-like
330 base : float, default: 10
331 Logarithmic base of the scale.
332 """
333 forward, inverse = functions
334 self.subs = None
335 self._transform = FuncTransform(forward, inverse) + LogTransform(base)
337 @property
338 def base(self):
339 return self._transform._b.base # Base of the LogTransform.
341 def get_transform(self):
342 """Return the `.Transform` associated with this scale."""
343 return self._transform
346class SymmetricalLogTransform(Transform):
347 input_dims = output_dims = 1
349 def __init__(self, base, linthresh, linscale):
350 super().__init__()
351 if base <= 1.0:
352 raise ValueError("'base' must be larger than 1")
353 if linthresh <= 0.0:
354 raise ValueError("'linthresh' must be positive")
355 if linscale <= 0.0:
356 raise ValueError("'linscale' must be positive")
357 self.base = base
358 self.linthresh = linthresh
359 self.linscale = linscale
360 self._linscale_adj = (linscale / (1.0 - self.base ** -1))
361 self._log_base = np.log(base)
363 def transform_non_affine(self, a):
364 abs_a = np.abs(a)
365 with np.errstate(divide="ignore", invalid="ignore"):
366 out = np.sign(a) * self.linthresh * (
367 self._linscale_adj +
368 np.log(abs_a / self.linthresh) / self._log_base)
369 inside = abs_a <= self.linthresh
370 out[inside] = a[inside] * self._linscale_adj
371 return out
373 def inverted(self):
374 return InvertedSymmetricalLogTransform(self.base, self.linthresh,
375 self.linscale)
378class InvertedSymmetricalLogTransform(Transform):
379 input_dims = output_dims = 1
381 def __init__(self, base, linthresh, linscale):
382 super().__init__()
383 symlog = SymmetricalLogTransform(base, linthresh, linscale)
384 self.base = base
385 self.linthresh = linthresh
386 self.invlinthresh = symlog.transform(linthresh)
387 self.linscale = linscale
388 self._linscale_adj = (linscale / (1.0 - self.base ** -1))
390 def transform_non_affine(self, a):
391 abs_a = np.abs(a)
392 with np.errstate(divide="ignore", invalid="ignore"):
393 out = np.sign(a) * self.linthresh * (
394 np.power(self.base,
395 abs_a / self.linthresh - self._linscale_adj))
396 inside = abs_a <= self.invlinthresh
397 out[inside] = a[inside] / self._linscale_adj
398 return out
400 def inverted(self):
401 return SymmetricalLogTransform(self.base,
402 self.linthresh, self.linscale)
405class SymmetricalLogScale(ScaleBase):
406 """
407 The symmetrical logarithmic scale is logarithmic in both the
408 positive and negative directions from the origin.
410 Since the values close to zero tend toward infinity, there is a
411 need to have a range around zero that is linear. The parameter
412 *linthresh* allows the user to specify the size of this range
413 (-*linthresh*, *linthresh*).
415 Parameters
416 ----------
417 base : float, default: 10
418 The base of the logarithm.
420 linthresh : float, default: 2
421 Defines the range ``(-x, x)``, within which the plot is linear.
422 This avoids having the plot go to infinity around zero.
424 subs : sequence of int
425 Where to place the subticks between each major tick.
426 For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place
427 8 logarithmically spaced minor ticks between each major tick.
429 linscale : float, optional
430 This allows the linear range ``(-linthresh, linthresh)`` to be
431 stretched relative to the logarithmic range. Its value is the number of
432 decades to use for each half of the linear range. For example, when
433 *linscale* == 1.0 (the default), the space used for the positive and
434 negative halves of the linear range will be equal to one decade in
435 the logarithmic range.
436 """
437 name = 'symlog'
439 def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1):
440 self._transform = SymmetricalLogTransform(base, linthresh, linscale)
441 self.subs = subs
443 base = property(lambda self: self._transform.base)
444 linthresh = property(lambda self: self._transform.linthresh)
445 linscale = property(lambda self: self._transform.linscale)
447 def set_default_locators_and_formatters(self, axis):
448 # docstring inherited
449 axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
450 axis.set_major_formatter(LogFormatterSciNotation(self.base))
451 axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
452 self.subs))
453 axis.set_minor_formatter(NullFormatter())
455 def get_transform(self):
456 """Return the `.SymmetricalLogTransform` associated with this scale."""
457 return self._transform
460class AsinhTransform(Transform):
461 """Inverse hyperbolic-sine transformation used by `.AsinhScale`"""
462 input_dims = output_dims = 1
464 def __init__(self, linear_width):
465 super().__init__()
466 if linear_width <= 0.0:
467 raise ValueError("Scale parameter 'linear_width' " +
468 "must be strictly positive")
469 self.linear_width = linear_width
471 def transform_non_affine(self, a):
472 return self.linear_width * np.arcsinh(a / self.linear_width)
474 def inverted(self):
475 return InvertedAsinhTransform(self.linear_width)
478class InvertedAsinhTransform(Transform):
479 """Hyperbolic sine transformation used by `.AsinhScale`"""
480 input_dims = output_dims = 1
482 def __init__(self, linear_width):
483 super().__init__()
484 self.linear_width = linear_width
486 def transform_non_affine(self, a):
487 return self.linear_width * np.sinh(a / self.linear_width)
489 def inverted(self):
490 return AsinhTransform(self.linear_width)
493class AsinhScale(ScaleBase):
494 """
495 A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh)
497 For values close to zero, this is essentially a linear scale,
498 but for large magnitude values (either positive or negative)
499 it is asymptotically logarithmic. The transition between these
500 linear and logarithmic regimes is smooth, and has no discontinuities
501 in the function gradient in contrast to
502 the `.SymmetricalLogScale` ("symlog") scale.
504 Specifically, the transformation of an axis coordinate :math:`a` is
505 :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0`
506 is the effective width of the linear region of the transformation.
507 In that region, the transformation is
508 :math:`a \\rightarrow a + \\mathcal{O}(a^3)`.
509 For large values of :math:`a` the transformation behaves as
510 :math:`a \\rightarrow a_0 \\, \\mathrm{sgn}(a) \\ln |a| + \\mathcal{O}(1)`.
512 .. note::
514 This API is provisional and may be revised in the future
515 based on early user feedback.
516 """
518 name = 'asinh'
520 auto_tick_multipliers = {
521 3: (2, ),
522 4: (2, ),
523 5: (2, ),
524 8: (2, 4),
525 10: (2, 5),
526 16: (2, 4, 8),
527 64: (4, 16),
528 1024: (256, 512)
529 }
531 def __init__(self, axis, *, linear_width=1.0,
532 base=10, subs='auto', **kwargs):
533 """
534 Parameters
535 ----------
536 linear_width : float, default: 1
537 The scale parameter (elsewhere referred to as :math:`a_0`)
538 defining the extent of the quasi-linear region,
539 and the coordinate values beyond which the transformation
540 becomes asymptotically logarithmic.
541 base : int, default: 10
542 The number base used for rounding tick locations
543 on a logarithmic scale. If this is less than one,
544 then rounding is to the nearest integer multiple
545 of powers of ten.
546 subs : sequence of int
547 Multiples of the number base used for minor ticks.
548 If set to 'auto', this will use built-in defaults,
549 e.g. (2, 5) for base=10.
550 """
551 super().__init__(axis)
552 self._transform = AsinhTransform(linear_width)
553 self._base = int(base)
554 if subs == 'auto':
555 self._subs = self.auto_tick_multipliers.get(self._base)
556 else:
557 self._subs = subs
559 linear_width = property(lambda self: self._transform.linear_width)
561 def get_transform(self):
562 return self._transform
564 def set_default_locators_and_formatters(self, axis):
565 axis.set(major_locator=AsinhLocator(self.linear_width,
566 base=self._base),
567 minor_locator=AsinhLocator(self.linear_width,
568 base=self._base,
569 subs=self._subs),
570 minor_formatter=NullFormatter())
571 if self._base > 1:
572 axis.set_major_formatter(LogFormatterSciNotation(self._base))
573 else:
574 axis.set_major_formatter('{x:.3g}'),
577class LogitTransform(Transform):
578 input_dims = output_dims = 1
580 def __init__(self, nonpositive='mask'):
581 super().__init__()
582 _api.check_in_list(['mask', 'clip'], nonpositive=nonpositive)
583 self._nonpositive = nonpositive
584 self._clip = {"clip": True, "mask": False}[nonpositive]
586 def transform_non_affine(self, a):
587 """logit transform (base 10), masked or clipped"""
588 with np.errstate(divide="ignore", invalid="ignore"):
589 out = np.log10(a / (1 - a))
590 if self._clip: # See LogTransform for choice of clip value.
591 out[a <= 0] = -1000
592 out[1 <= a] = 1000
593 return out
595 def inverted(self):
596 return LogisticTransform(self._nonpositive)
598 def __str__(self):
599 return "{}({!r})".format(type(self).__name__, self._nonpositive)
602class LogisticTransform(Transform):
603 input_dims = output_dims = 1
605 def __init__(self, nonpositive='mask'):
606 super().__init__()
607 self._nonpositive = nonpositive
609 def transform_non_affine(self, a):
610 """logistic transform (base 10)"""
611 return 1.0 / (1 + 10**(-a))
613 def inverted(self):
614 return LogitTransform(self._nonpositive)
616 def __str__(self):
617 return "{}({!r})".format(type(self).__name__, self._nonpositive)
620class LogitScale(ScaleBase):
621 """
622 Logit scale for data between zero and one, both excluded.
624 This scale is similar to a log scale close to zero and to one, and almost
625 linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
626 """
627 name = 'logit'
629 def __init__(self, axis, nonpositive='mask', *,
630 one_half=r"\frac{1}{2}", use_overline=False):
631 r"""
632 Parameters
633 ----------
634 axis : `matplotlib.axis.Axis`
635 Currently unused.
636 nonpositive : {'mask', 'clip'}
637 Determines the behavior for values beyond the open interval ]0, 1[.
638 They can either be masked as invalid, or clipped to a number very
639 close to 0 or 1.
640 use_overline : bool, default: False
641 Indicate the usage of survival notation (\overline{x}) in place of
642 standard notation (1-x) for probability close to one.
643 one_half : str, default: r"\frac{1}{2}"
644 The string used for ticks formatter to represent 1/2.
645 """
646 self._transform = LogitTransform(nonpositive)
647 self._use_overline = use_overline
648 self._one_half = one_half
650 def get_transform(self):
651 """Return the `.LogitTransform` associated with this scale."""
652 return self._transform
654 def set_default_locators_and_formatters(self, axis):
655 # docstring inherited
656 # ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
657 axis.set_major_locator(LogitLocator())
658 axis.set_major_formatter(
659 LogitFormatter(
660 one_half=self._one_half,
661 use_overline=self._use_overline
662 )
663 )
664 axis.set_minor_locator(LogitLocator(minor=True))
665 axis.set_minor_formatter(
666 LogitFormatter(
667 minor=True,
668 one_half=self._one_half,
669 use_overline=self._use_overline
670 )
671 )
673 def limit_range_for_scale(self, vmin, vmax, minpos):
674 """
675 Limit the domain to values between 0 and 1 (excluded).
676 """
677 if not np.isfinite(minpos):
678 minpos = 1e-7 # Should rarely (if ever) have a visible effect.
679 return (minpos if vmin <= 0 else vmin,
680 1 - minpos if vmax >= 1 else vmax)
683_scale_mapping = {
684 'linear': LinearScale,
685 'log': LogScale,
686 'symlog': SymmetricalLogScale,
687 'asinh': AsinhScale,
688 'logit': LogitScale,
689 'function': FuncScale,
690 'functionlog': FuncScaleLog,
691 }
694def get_scale_names():
695 """Return the names of the available scales."""
696 return sorted(_scale_mapping)
699def scale_factory(scale, axis, **kwargs):
700 """
701 Return a scale class by name.
703 Parameters
704 ----------
705 scale : {%(names)s}
706 axis : `matplotlib.axis.Axis`
707 """
708 if scale != scale.lower():
709 _api.warn_deprecated(
710 "3.5", message="Support for case-insensitive scales is deprecated "
711 "since %(since)s and support will be removed %(removal)s.")
712 scale = scale.lower()
713 scale_cls = _api.check_getitem(_scale_mapping, scale=scale)
714 return scale_cls(axis, **kwargs)
717if scale_factory.__doc__:
718 scale_factory.__doc__ = scale_factory.__doc__ % {
719 "names": ", ".join(map(repr, get_scale_names()))}
722def register_scale(scale_class):
723 """
724 Register a new kind of scale.
726 Parameters
727 ----------
728 scale_class : subclass of `ScaleBase`
729 The scale to register.
730 """
731 _scale_mapping[scale_class.name] = scale_class
734def _get_scale_docs():
735 """
736 Helper function for generating docstrings related to scales.
737 """
738 docs = []
739 for name, scale_class in _scale_mapping.items():
740 docstring = inspect.getdoc(scale_class.__init__) or ""
741 docs.extend([
742 f" {name!r}",
743 "",
744 textwrap.indent(docstring, " " * 8),
745 ""
746 ])
747 return "\n".join(docs)
750_docstring.interpd.update(
751 scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]),
752 scale_docs=_get_scale_docs().rstrip(),
753 )