Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/scale.py: 45%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 implementers.
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 """ # noqa: D419
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 @_api.rename_parameter("3.8", "a", "values")
217 def transform_non_affine(self, values):
218 # Ignore invalid values due to nans being passed to the transform.
219 with np.errstate(divide="ignore", invalid="ignore"):
220 log = {np.e: np.log, 2: np.log2, 10: np.log10}.get(self.base)
221 if log: # If possible, do everything in a single call to NumPy.
222 out = log(values)
223 else:
224 out = np.log(values)
225 out /= np.log(self.base)
226 if self._clip:
227 # SVG spec says that conforming viewers must support values up
228 # to 3.4e38 (C float); however experiments suggest that
229 # Inkscape (which uses cairo for rendering) runs into cairo's
230 # 24-bit limit (which is apparently shared by Agg).
231 # Ghostscript (used for pdf rendering appears to overflow even
232 # earlier, with the max value around 2 ** 15 for the tests to
233 # pass. On the other hand, in practice, we want to clip beyond
234 # np.log10(np.nextafter(0, 1)) ~ -323
235 # so 1000 seems safe.
236 out[values <= 0] = -1000
237 return out
239 def inverted(self):
240 return InvertedLogTransform(self.base)
243class InvertedLogTransform(Transform):
244 input_dims = output_dims = 1
246 def __init__(self, base):
247 super().__init__()
248 self.base = base
250 def __str__(self):
251 return f"{type(self).__name__}(base={self.base})"
253 @_api.rename_parameter("3.8", "a", "values")
254 def transform_non_affine(self, values):
255 return np.power(self.base, values)
257 def inverted(self):
258 return LogTransform(self.base)
261class LogScale(ScaleBase):
262 """
263 A standard logarithmic scale. Care is taken to only plot positive values.
264 """
265 name = 'log'
267 def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"):
268 """
269 Parameters
270 ----------
271 axis : `~matplotlib.axis.Axis`
272 The axis for the scale.
273 base : float, default: 10
274 The base of the logarithm.
275 nonpositive : {'clip', 'mask'}, default: 'clip'
276 Determines the behavior for non-positive values. They can either
277 be masked as invalid, or clipped to a very small positive number.
278 subs : sequence of int, default: None
279 Where to place the subticks between each major tick. For example,
280 in a log10 scale, ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place 8
281 logarithmically spaced minor ticks between each major tick.
282 """
283 self._transform = LogTransform(base, nonpositive)
284 self.subs = subs
286 base = property(lambda self: self._transform.base)
288 def set_default_locators_and_formatters(self, axis):
289 # docstring inherited
290 axis.set_major_locator(LogLocator(self.base))
291 axis.set_major_formatter(LogFormatterSciNotation(self.base))
292 axis.set_minor_locator(LogLocator(self.base, self.subs))
293 axis.set_minor_formatter(
294 LogFormatterSciNotation(self.base,
295 labelOnlyBase=(self.subs is not None)))
297 def get_transform(self):
298 """Return the `.LogTransform` associated with this scale."""
299 return self._transform
301 def limit_range_for_scale(self, vmin, vmax, minpos):
302 """Limit the domain to positive values."""
303 if not np.isfinite(minpos):
304 minpos = 1e-300 # Should rarely (if ever) have a visible effect.
306 return (minpos if vmin <= 0 else vmin,
307 minpos if vmax <= 0 else vmax)
310class FuncScaleLog(LogScale):
311 """
312 Provide an arbitrary scale with user-supplied function for the axis and
313 then put on a logarithmic axes.
314 """
316 name = 'functionlog'
318 def __init__(self, axis, functions, base=10):
319 """
320 Parameters
321 ----------
322 axis : `~matplotlib.axis.Axis`
323 The axis for the scale.
324 functions : (callable, callable)
325 two-tuple of the forward and inverse functions for the scale.
326 The forward function must be monotonic.
328 Both functions must have the signature::
330 def forward(values: array-like) -> array-like
332 base : float, default: 10
333 Logarithmic base of the scale.
334 """
335 forward, inverse = functions
336 self.subs = None
337 self._transform = FuncTransform(forward, inverse) + LogTransform(base)
339 @property
340 def base(self):
341 return self._transform._b.base # Base of the LogTransform.
343 def get_transform(self):
344 """Return the `.Transform` associated with this scale."""
345 return self._transform
348class SymmetricalLogTransform(Transform):
349 input_dims = output_dims = 1
351 def __init__(self, base, linthresh, linscale):
352 super().__init__()
353 if base <= 1.0:
354 raise ValueError("'base' must be larger than 1")
355 if linthresh <= 0.0:
356 raise ValueError("'linthresh' must be positive")
357 if linscale <= 0.0:
358 raise ValueError("'linscale' must be positive")
359 self.base = base
360 self.linthresh = linthresh
361 self.linscale = linscale
362 self._linscale_adj = (linscale / (1.0 - self.base ** -1))
363 self._log_base = np.log(base)
365 @_api.rename_parameter("3.8", "a", "values")
366 def transform_non_affine(self, values):
367 abs_a = np.abs(values)
368 with np.errstate(divide="ignore", invalid="ignore"):
369 out = np.sign(values) * self.linthresh * (
370 self._linscale_adj +
371 np.log(abs_a / self.linthresh) / self._log_base)
372 inside = abs_a <= self.linthresh
373 out[inside] = values[inside] * self._linscale_adj
374 return out
376 def inverted(self):
377 return InvertedSymmetricalLogTransform(self.base, self.linthresh,
378 self.linscale)
381class InvertedSymmetricalLogTransform(Transform):
382 input_dims = output_dims = 1
384 def __init__(self, base, linthresh, linscale):
385 super().__init__()
386 symlog = SymmetricalLogTransform(base, linthresh, linscale)
387 self.base = base
388 self.linthresh = linthresh
389 self.invlinthresh = symlog.transform(linthresh)
390 self.linscale = linscale
391 self._linscale_adj = (linscale / (1.0 - self.base ** -1))
393 @_api.rename_parameter("3.8", "a", "values")
394 def transform_non_affine(self, values):
395 abs_a = np.abs(values)
396 with np.errstate(divide="ignore", invalid="ignore"):
397 out = np.sign(values) * self.linthresh * (
398 np.power(self.base,
399 abs_a / self.linthresh - self._linscale_adj))
400 inside = abs_a <= self.invlinthresh
401 out[inside] = values[inside] / self._linscale_adj
402 return out
404 def inverted(self):
405 return SymmetricalLogTransform(self.base,
406 self.linthresh, self.linscale)
409class SymmetricalLogScale(ScaleBase):
410 """
411 The symmetrical logarithmic scale is logarithmic in both the
412 positive and negative directions from the origin.
414 Since the values close to zero tend toward infinity, there is a
415 need to have a range around zero that is linear. The parameter
416 *linthresh* allows the user to specify the size of this range
417 (-*linthresh*, *linthresh*).
419 Parameters
420 ----------
421 base : float, default: 10
422 The base of the logarithm.
424 linthresh : float, default: 2
425 Defines the range ``(-x, x)``, within which the plot is linear.
426 This avoids having the plot go to infinity around zero.
428 subs : sequence of int
429 Where to place the subticks between each major tick.
430 For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place
431 8 logarithmically spaced minor ticks between each major tick.
433 linscale : float, optional
434 This allows the linear range ``(-linthresh, linthresh)`` to be
435 stretched relative to the logarithmic range. Its value is the number of
436 decades to use for each half of the linear range. For example, when
437 *linscale* == 1.0 (the default), the space used for the positive and
438 negative halves of the linear range will be equal to one decade in
439 the logarithmic range.
440 """
441 name = 'symlog'
443 def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1):
444 self._transform = SymmetricalLogTransform(base, linthresh, linscale)
445 self.subs = subs
447 base = property(lambda self: self._transform.base)
448 linthresh = property(lambda self: self._transform.linthresh)
449 linscale = property(lambda self: self._transform.linscale)
451 def set_default_locators_and_formatters(self, axis):
452 # docstring inherited
453 axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
454 axis.set_major_formatter(LogFormatterSciNotation(self.base))
455 axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
456 self.subs))
457 axis.set_minor_formatter(NullFormatter())
459 def get_transform(self):
460 """Return the `.SymmetricalLogTransform` associated with this scale."""
461 return self._transform
464class AsinhTransform(Transform):
465 """Inverse hyperbolic-sine transformation used by `.AsinhScale`"""
466 input_dims = output_dims = 1
468 def __init__(self, linear_width):
469 super().__init__()
470 if linear_width <= 0.0:
471 raise ValueError("Scale parameter 'linear_width' " +
472 "must be strictly positive")
473 self.linear_width = linear_width
475 @_api.rename_parameter("3.8", "a", "values")
476 def transform_non_affine(self, values):
477 return self.linear_width * np.arcsinh(values / self.linear_width)
479 def inverted(self):
480 return InvertedAsinhTransform(self.linear_width)
483class InvertedAsinhTransform(Transform):
484 """Hyperbolic sine transformation used by `.AsinhScale`"""
485 input_dims = output_dims = 1
487 def __init__(self, linear_width):
488 super().__init__()
489 self.linear_width = linear_width
491 @_api.rename_parameter("3.8", "a", "values")
492 def transform_non_affine(self, values):
493 return self.linear_width * np.sinh(values / self.linear_width)
495 def inverted(self):
496 return AsinhTransform(self.linear_width)
499class AsinhScale(ScaleBase):
500 """
501 A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh)
503 For values close to zero, this is essentially a linear scale,
504 but for large magnitude values (either positive or negative)
505 it is asymptotically logarithmic. The transition between these
506 linear and logarithmic regimes is smooth, and has no discontinuities
507 in the function gradient in contrast to
508 the `.SymmetricalLogScale` ("symlog") scale.
510 Specifically, the transformation of an axis coordinate :math:`a` is
511 :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0`
512 is the effective width of the linear region of the transformation.
513 In that region, the transformation is
514 :math:`a \\rightarrow a + \\mathcal{O}(a^3)`.
515 For large values of :math:`a` the transformation behaves as
516 :math:`a \\rightarrow a_0 \\, \\mathrm{sgn}(a) \\ln |a| + \\mathcal{O}(1)`.
518 .. note::
520 This API is provisional and may be revised in the future
521 based on early user feedback.
522 """
524 name = 'asinh'
526 auto_tick_multipliers = {
527 3: (2, ),
528 4: (2, ),
529 5: (2, ),
530 8: (2, 4),
531 10: (2, 5),
532 16: (2, 4, 8),
533 64: (4, 16),
534 1024: (256, 512)
535 }
537 def __init__(self, axis, *, linear_width=1.0,
538 base=10, subs='auto', **kwargs):
539 """
540 Parameters
541 ----------
542 linear_width : float, default: 1
543 The scale parameter (elsewhere referred to as :math:`a_0`)
544 defining the extent of the quasi-linear region,
545 and the coordinate values beyond which the transformation
546 becomes asymptotically logarithmic.
547 base : int, default: 10
548 The number base used for rounding tick locations
549 on a logarithmic scale. If this is less than one,
550 then rounding is to the nearest integer multiple
551 of powers of ten.
552 subs : sequence of int
553 Multiples of the number base used for minor ticks.
554 If set to 'auto', this will use built-in defaults,
555 e.g. (2, 5) for base=10.
556 """
557 super().__init__(axis)
558 self._transform = AsinhTransform(linear_width)
559 self._base = int(base)
560 if subs == 'auto':
561 self._subs = self.auto_tick_multipliers.get(self._base)
562 else:
563 self._subs = subs
565 linear_width = property(lambda self: self._transform.linear_width)
567 def get_transform(self):
568 return self._transform
570 def set_default_locators_and_formatters(self, axis):
571 axis.set(major_locator=AsinhLocator(self.linear_width,
572 base=self._base),
573 minor_locator=AsinhLocator(self.linear_width,
574 base=self._base,
575 subs=self._subs),
576 minor_formatter=NullFormatter())
577 if self._base > 1:
578 axis.set_major_formatter(LogFormatterSciNotation(self._base))
579 else:
580 axis.set_major_formatter('{x:.3g}')
583class LogitTransform(Transform):
584 input_dims = output_dims = 1
586 def __init__(self, nonpositive='mask'):
587 super().__init__()
588 _api.check_in_list(['mask', 'clip'], nonpositive=nonpositive)
589 self._nonpositive = nonpositive
590 self._clip = {"clip": True, "mask": False}[nonpositive]
592 @_api.rename_parameter("3.8", "a", "values")
593 def transform_non_affine(self, values):
594 """logit transform (base 10), masked or clipped"""
595 with np.errstate(divide="ignore", invalid="ignore"):
596 out = np.log10(values / (1 - values))
597 if self._clip: # See LogTransform for choice of clip value.
598 out[values <= 0] = -1000
599 out[1 <= values] = 1000
600 return out
602 def inverted(self):
603 return LogisticTransform(self._nonpositive)
605 def __str__(self):
606 return f"{type(self).__name__}({self._nonpositive!r})"
609class LogisticTransform(Transform):
610 input_dims = output_dims = 1
612 def __init__(self, nonpositive='mask'):
613 super().__init__()
614 self._nonpositive = nonpositive
616 @_api.rename_parameter("3.8", "a", "values")
617 def transform_non_affine(self, values):
618 """logistic transform (base 10)"""
619 return 1.0 / (1 + 10**(-values))
621 def inverted(self):
622 return LogitTransform(self._nonpositive)
624 def __str__(self):
625 return f"{type(self).__name__}({self._nonpositive!r})"
628class LogitScale(ScaleBase):
629 """
630 Logit scale for data between zero and one, both excluded.
632 This scale is similar to a log scale close to zero and to one, and almost
633 linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
634 """
635 name = 'logit'
637 def __init__(self, axis, nonpositive='mask', *,
638 one_half=r"\frac{1}{2}", use_overline=False):
639 r"""
640 Parameters
641 ----------
642 axis : `~matplotlib.axis.Axis`
643 Currently unused.
644 nonpositive : {'mask', 'clip'}
645 Determines the behavior for values beyond the open interval ]0, 1[.
646 They can either be masked as invalid, or clipped to a number very
647 close to 0 or 1.
648 use_overline : bool, default: False
649 Indicate the usage of survival notation (\overline{x}) in place of
650 standard notation (1-x) for probability close to one.
651 one_half : str, default: r"\frac{1}{2}"
652 The string used for ticks formatter to represent 1/2.
653 """
654 self._transform = LogitTransform(nonpositive)
655 self._use_overline = use_overline
656 self._one_half = one_half
658 def get_transform(self):
659 """Return the `.LogitTransform` associated with this scale."""
660 return self._transform
662 def set_default_locators_and_formatters(self, axis):
663 # docstring inherited
664 # ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
665 axis.set_major_locator(LogitLocator())
666 axis.set_major_formatter(
667 LogitFormatter(
668 one_half=self._one_half,
669 use_overline=self._use_overline
670 )
671 )
672 axis.set_minor_locator(LogitLocator(minor=True))
673 axis.set_minor_formatter(
674 LogitFormatter(
675 minor=True,
676 one_half=self._one_half,
677 use_overline=self._use_overline
678 )
679 )
681 def limit_range_for_scale(self, vmin, vmax, minpos):
682 """
683 Limit the domain to values between 0 and 1 (excluded).
684 """
685 if not np.isfinite(minpos):
686 minpos = 1e-7 # Should rarely (if ever) have a visible effect.
687 return (minpos if vmin <= 0 else vmin,
688 1 - minpos if vmax >= 1 else vmax)
691_scale_mapping = {
692 'linear': LinearScale,
693 'log': LogScale,
694 'symlog': SymmetricalLogScale,
695 'asinh': AsinhScale,
696 'logit': LogitScale,
697 'function': FuncScale,
698 'functionlog': FuncScaleLog,
699 }
702def get_scale_names():
703 """Return the names of the available scales."""
704 return sorted(_scale_mapping)
707def scale_factory(scale, axis, **kwargs):
708 """
709 Return a scale class by name.
711 Parameters
712 ----------
713 scale : {%(names)s}
714 axis : `~matplotlib.axis.Axis`
715 """
716 scale_cls = _api.check_getitem(_scale_mapping, scale=scale)
717 return scale_cls(axis, **kwargs)
720if scale_factory.__doc__:
721 scale_factory.__doc__ = scale_factory.__doc__ % {
722 "names": ", ".join(map(repr, get_scale_names()))}
725def register_scale(scale_class):
726 """
727 Register a new kind of scale.
729 Parameters
730 ----------
731 scale_class : subclass of `ScaleBase`
732 The scale to register.
733 """
734 _scale_mapping[scale_class.name] = scale_class
737def _get_scale_docs():
738 """
739 Helper function for generating docstrings related to scales.
740 """
741 docs = []
742 for name, scale_class in _scale_mapping.items():
743 docstring = inspect.getdoc(scale_class.__init__) or ""
744 docs.extend([
745 f" {name!r}",
746 "",
747 textwrap.indent(docstring, " " * 8),
748 ""
749 ])
750 return "\n".join(docs)
753_docstring.interpd.update(
754 scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]),
755 scale_docs=_get_scale_docs().rstrip(),
756 )