Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/scale.py : 45%

Hot-keys on this page
r m x p 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.
4They are attached to an `~.axis.Axis` and hold a `.Transform`, which is
5responsible for the actual data transformation.
7See also `.axes.Axes.set_xscale` and the scales examples in the documentation.
8"""
10import inspect
11import textwrap
13import numpy as np
14from numpy import ma
16from matplotlib import cbook, docstring, rcParams
17from matplotlib.ticker import (
18 NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter,
19 NullLocator, LogLocator, AutoLocator, AutoMinorLocator,
20 SymmetricalLogLocator, LogitLocator)
21from matplotlib.transforms import Transform, IdentityTransform
22from matplotlib.cbook import warn_deprecated
25class ScaleBase:
26 """
27 The base class for all scales.
29 Scales are separable transformations, working on a single dimension.
31 Any subclasses will want to override:
33 - :attr:`name`
34 - :meth:`get_transform`
35 - :meth:`set_default_locators_and_formatters`
37 And optionally:
39 - :meth:`limit_range_for_scale`
41 """
43 def __init__(self, axis, **kwargs):
44 r"""
45 Construct a new scale.
47 Notes
48 -----
49 The following note is for scale implementors.
51 For back-compatibility reasons, scales take an `~matplotlib.axis.Axis`
52 object as first argument. However, this argument should not
53 be used: a single scale object should be usable by multiple
54 `~matplotlib.axis.Axis`\es at the same time.
55 """
56 if kwargs:
57 warn_deprecated(
58 '3.2.0',
59 message=(
60 f"ScaleBase got an unexpected keyword "
61 f"argument {next(iter(kwargs))!r}. "
62 'In the future this will raise TypeError')
63 )
65 def get_transform(self):
66 """
67 Return the :class:`~matplotlib.transforms.Transform` object
68 associated with this scale.
69 """
70 raise NotImplementedError()
72 def set_default_locators_and_formatters(self, axis):
73 """
74 Set the locators and formatters of *axis* to instances suitable for
75 this scale.
76 """
77 raise NotImplementedError()
79 def limit_range_for_scale(self, vmin, vmax, minpos):
80 """
81 Returns the range *vmin*, *vmax*, possibly limited to the
82 domain supported by this scale.
84 *minpos* should be the minimum positive value in the data.
85 This is used by log scales to determine a minimum value.
86 """
87 return vmin, vmax
90class LinearScale(ScaleBase):
91 """
92 The default linear scale.
93 """
95 name = 'linear'
97 def __init__(self, axis, **kwargs):
98 # This method is present only to prevent inheritance of the base class'
99 # constructor docstring, which would otherwise end up interpolated into
100 # the docstring of Axis.set_scale.
101 """
102 """
103 super().__init__(axis, **kwargs)
105 def set_default_locators_and_formatters(self, axis):
106 # docstring inherited
107 axis.set_major_locator(AutoLocator())
108 axis.set_major_formatter(ScalarFormatter())
109 axis.set_minor_formatter(NullFormatter())
110 # update the minor locator for x and y axis based on rcParams
111 if (axis.axis_name == 'x' and rcParams['xtick.minor.visible']
112 or axis.axis_name == 'y' and rcParams['ytick.minor.visible']):
113 axis.set_minor_locator(AutoMinorLocator())
114 else:
115 axis.set_minor_locator(NullLocator())
117 def get_transform(self):
118 """
119 Return the transform for linear scaling, which is just the
120 `~matplotlib.transforms.IdentityTransform`.
121 """
122 return IdentityTransform()
125class FuncTransform(Transform):
126 """
127 A simple transform that takes and arbitrary function for the
128 forward and inverse transform.
129 """
131 input_dims = output_dims = 1
133 def __init__(self, forward, inverse):
134 """
135 Parameters
136 ----------
137 forward : callable
138 The forward function for the transform. This function must have
139 an inverse and, for best behavior, be monotonic.
140 It must have the signature::
142 def forward(values: array-like) -> array-like
144 inverse : callable
145 The inverse of the forward function. Signature as ``forward``.
146 """
147 super().__init__()
148 if callable(forward) and callable(inverse):
149 self._forward = forward
150 self._inverse = inverse
151 else:
152 raise ValueError('arguments to FuncTransform must be functions')
154 def transform_non_affine(self, values):
155 return self._forward(values)
157 def inverted(self):
158 return FuncTransform(self._inverse, self._forward)
161class FuncScale(ScaleBase):
162 """
163 Provide an arbitrary scale with user-supplied function for the axis.
164 """
166 name = 'function'
168 def __init__(self, axis, functions):
169 """
170 Parameters
171 ----------
172 axis : `~matplotlib.axis.Axis`
173 The axis for the scale.
174 functions : (callable, callable)
175 two-tuple of the forward and inverse functions for the scale.
176 The forward function must be monotonic.
178 Both functions must have the signature::
180 def forward(values: array-like) -> array-like
181 """
182 forward, inverse = functions
183 transform = FuncTransform(forward, inverse)
184 self._transform = transform
186 def get_transform(self):
187 """Return the `.FuncTransform` associated with this scale."""
188 return self._transform
190 def set_default_locators_and_formatters(self, axis):
191 # docstring inherited
192 axis.set_major_locator(AutoLocator())
193 axis.set_major_formatter(ScalarFormatter())
194 axis.set_minor_formatter(NullFormatter())
195 # update the minor locator for x and y axis based on rcParams
196 if (axis.axis_name == 'x' and rcParams['xtick.minor.visible']
197 or axis.axis_name == 'y' and rcParams['ytick.minor.visible']):
198 axis.set_minor_locator(AutoMinorLocator())
199 else:
200 axis.set_minor_locator(NullLocator())
203@cbook.deprecated("3.1", alternative="LogTransform")
204class LogTransformBase(Transform):
205 input_dims = output_dims = 1
207 def __init__(self, nonpos='clip'):
208 Transform.__init__(self)
209 self._clip = {"clip": True, "mask": False}[nonpos]
211 def transform_non_affine(self, a):
212 return LogTransform.transform_non_affine(self, a)
214 def __str__(self):
215 return "{}({!r})".format(
216 type(self).__name__, "clip" if self._clip else "mask")
219@cbook.deprecated("3.1", alternative="InvertedLogTransform")
220class InvertedLogTransformBase(Transform):
221 input_dims = output_dims = 1
223 def transform_non_affine(self, a):
224 return ma.power(self.base, a)
226 def __str__(self):
227 return "{}()".format(type(self).__name__)
230@cbook.deprecated("3.1", alternative="LogTransform")
231class Log10Transform(LogTransformBase):
232 base = 10.0
234 def inverted(self):
235 return InvertedLog10Transform()
238@cbook.deprecated("3.1", alternative="InvertedLogTransform")
239class InvertedLog10Transform(InvertedLogTransformBase):
240 base = 10.0
242 def inverted(self):
243 return Log10Transform()
246@cbook.deprecated("3.1", alternative="LogTransform")
247class Log2Transform(LogTransformBase):
248 base = 2.0
250 def inverted(self):
251 return InvertedLog2Transform()
254@cbook.deprecated("3.1", alternative="InvertedLogTransform")
255class InvertedLog2Transform(InvertedLogTransformBase):
256 base = 2.0
258 def inverted(self):
259 return Log2Transform()
262@cbook.deprecated("3.1", alternative="LogTransform")
263class NaturalLogTransform(LogTransformBase):
264 base = np.e
266 def inverted(self):
267 return InvertedNaturalLogTransform()
270@cbook.deprecated("3.1", alternative="InvertedLogTransform")
271class InvertedNaturalLogTransform(InvertedLogTransformBase):
272 base = np.e
274 def inverted(self):
275 return NaturalLogTransform()
278class LogTransform(Transform):
279 input_dims = output_dims = 1
281 def __init__(self, base, nonpos='clip'):
282 Transform.__init__(self)
283 self.base = base
284 self._clip = {"clip": True, "mask": False}[nonpos]
286 def __str__(self):
287 return "{}(base={}, nonpos={!r})".format(
288 type(self).__name__, self.base, "clip" if self._clip else "mask")
290 def transform_non_affine(self, a):
291 # Ignore invalid values due to nans being passed to the transform.
292 with np.errstate(divide="ignore", invalid="ignore"):
293 log = {np.e: np.log, 2: np.log2, 10: np.log10}.get(self.base)
294 if log: # If possible, do everything in a single call to NumPy.
295 out = log(a)
296 else:
297 out = np.log(a)
298 out /= np.log(self.base)
299 if self._clip:
300 # SVG spec says that conforming viewers must support values up
301 # to 3.4e38 (C float); however experiments suggest that
302 # Inkscape (which uses cairo for rendering) runs into cairo's
303 # 24-bit limit (which is apparently shared by Agg).
304 # Ghostscript (used for pdf rendering appears to overflow even
305 # earlier, with the max value around 2 ** 15 for the tests to
306 # pass. On the other hand, in practice, we want to clip beyond
307 # np.log10(np.nextafter(0, 1)) ~ -323
308 # so 1000 seems safe.
309 out[a <= 0] = -1000
310 return out
312 def inverted(self):
313 return InvertedLogTransform(self.base)
316class InvertedLogTransform(Transform):
317 input_dims = output_dims = 1
319 def __init__(self, base):
320 Transform.__init__(self)
321 self.base = base
323 def __str__(self):
324 return "{}(base={})".format(type(self).__name__, self.base)
326 def transform_non_affine(self, a):
327 return ma.power(self.base, a)
329 def inverted(self):
330 return LogTransform(self.base)
333class LogScale(ScaleBase):
334 """
335 A standard logarithmic scale. Care is taken to only plot positive values.
336 """
337 name = 'log'
339 # compatibility shim
340 LogTransformBase = LogTransformBase
341 Log10Transform = Log10Transform
342 InvertedLog10Transform = InvertedLog10Transform
343 Log2Transform = Log2Transform
344 InvertedLog2Transform = InvertedLog2Transform
345 NaturalLogTransform = NaturalLogTransform
346 InvertedNaturalLogTransform = InvertedNaturalLogTransform
347 LogTransform = LogTransform
348 InvertedLogTransform = InvertedLogTransform
350 def __init__(self, axis, **kwargs):
351 """
352 Parameters
353 ----------
354 axis : `~matplotlib.axis.Axis`
355 The axis for the scale.
356 basex, basey : float, default: 10
357 The base of the logarithm.
358 nonposx, nonposy : {'clip', 'mask'}, default: 'clip'
359 Determines the behavior for non-positive values. They can either
360 be masked as invalid, or clipped to a very small positive number.
361 subsx, subsy : sequence of int, default: None
362 Where to place the subticks between each major tick.
363 For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]``
364 will place 8 logarithmically spaced minor ticks between
365 each major tick.
366 """
367 if axis.axis_name == 'x':
368 base = kwargs.pop('basex', 10.0)
369 subs = kwargs.pop('subsx', None)
370 nonpos = kwargs.pop('nonposx', 'clip')
371 cbook._check_in_list(['mask', 'clip'], nonposx=nonpos)
372 else:
373 base = kwargs.pop('basey', 10.0)
374 subs = kwargs.pop('subsy', None)
375 nonpos = kwargs.pop('nonposy', 'clip')
376 cbook._check_in_list(['mask', 'clip'], nonposy=nonpos)
378 if kwargs:
379 raise TypeError(f"LogScale got an unexpected keyword "
380 f"argument {next(iter(kwargs))!r}")
382 if base <= 0 or base == 1:
383 raise ValueError('The log base cannot be <= 0 or == 1')
385 self._transform = LogTransform(base, nonpos)
386 self.subs = subs
388 @property
389 def base(self):
390 return self._transform.base
392 def set_default_locators_and_formatters(self, axis):
393 # docstring inherited
394 axis.set_major_locator(LogLocator(self.base))
395 axis.set_major_formatter(LogFormatterSciNotation(self.base))
396 axis.set_minor_locator(LogLocator(self.base, self.subs))
397 axis.set_minor_formatter(
398 LogFormatterSciNotation(self.base,
399 labelOnlyBase=(self.subs is not None)))
401 def get_transform(self):
402 """Return the `.LogTransform` associated with this scale."""
403 return self._transform
405 def limit_range_for_scale(self, vmin, vmax, minpos):
406 """Limit the domain to positive values."""
407 if not np.isfinite(minpos):
408 minpos = 1e-300 # Should rarely (if ever) have a visible effect.
410 return (minpos if vmin <= 0 else vmin,
411 minpos if vmax <= 0 else vmax)
414class FuncScaleLog(LogScale):
415 """
416 Provide an arbitrary scale with user-supplied function for the axis and
417 then put on a logarithmic axes.
418 """
420 name = 'functionlog'
422 def __init__(self, axis, functions, base=10):
423 """
424 Parameters
425 ----------
426 axis : `matplotlib.axis.Axis`
427 The axis for the scale.
428 functions : (callable, callable)
429 two-tuple of the forward and inverse functions for the scale.
430 The forward function must be monotonic.
432 Both functions must have the signature::
434 def forward(values: array-like) -> array-like
436 base : float
437 logarithmic base of the scale (default = 10)
439 """
440 forward, inverse = functions
441 self.subs = None
442 self._transform = FuncTransform(forward, inverse) + LogTransform(base)
444 @property
445 def base(self):
446 return self._transform._b.base # Base of the LogTransform.
448 def get_transform(self):
449 """Return the `.Transform` associated with this scale."""
450 return self._transform
453class SymmetricalLogTransform(Transform):
454 input_dims = output_dims = 1
456 def __init__(self, base, linthresh, linscale):
457 Transform.__init__(self)
458 self.base = base
459 self.linthresh = linthresh
460 self.linscale = linscale
461 self._linscale_adj = (linscale / (1.0 - self.base ** -1))
462 self._log_base = np.log(base)
464 def transform_non_affine(self, a):
465 abs_a = np.abs(a)
466 with np.errstate(divide="ignore", invalid="ignore"):
467 out = np.sign(a) * self.linthresh * (
468 self._linscale_adj +
469 np.log(abs_a / self.linthresh) / self._log_base)
470 inside = abs_a <= self.linthresh
471 out[inside] = a[inside] * self._linscale_adj
472 return out
474 def inverted(self):
475 return InvertedSymmetricalLogTransform(self.base, self.linthresh,
476 self.linscale)
479class InvertedSymmetricalLogTransform(Transform):
480 input_dims = output_dims = 1
482 def __init__(self, base, linthresh, linscale):
483 Transform.__init__(self)
484 symlog = SymmetricalLogTransform(base, linthresh, linscale)
485 self.base = base
486 self.linthresh = linthresh
487 self.invlinthresh = symlog.transform(linthresh)
488 self.linscale = linscale
489 self._linscale_adj = (linscale / (1.0 - self.base ** -1))
491 def transform_non_affine(self, a):
492 abs_a = np.abs(a)
493 with np.errstate(divide="ignore", invalid="ignore"):
494 out = np.sign(a) * self.linthresh * (
495 np.power(self.base,
496 abs_a / self.linthresh - self._linscale_adj))
497 inside = abs_a <= self.invlinthresh
498 out[inside] = a[inside] / self._linscale_adj
499 return out
501 def inverted(self):
502 return SymmetricalLogTransform(self.base,
503 self.linthresh, self.linscale)
506class SymmetricalLogScale(ScaleBase):
507 """
508 The symmetrical logarithmic scale is logarithmic in both the
509 positive and negative directions from the origin.
511 Since the values close to zero tend toward infinity, there is a
512 need to have a range around zero that is linear. The parameter
513 *linthresh* allows the user to specify the size of this range
514 (-*linthresh*, *linthresh*).
516 Parameters
517 ----------
518 basex, basey : float
519 The base of the logarithm. Defaults to 10.
521 linthreshx, linthreshy : float
522 Defines the range ``(-x, x)``, within which the plot is linear.
523 This avoids having the plot go to infinity around zero. Defaults to 2.
525 subsx, subsy : sequence of int
526 Where to place the subticks between each major tick.
527 For example, in a log10 scale: ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place
528 8 logarithmically spaced minor ticks between each major tick.
530 linscalex, linscaley : float, optional
531 This allows the linear range ``(-linthresh, linthresh)`` to be
532 stretched relative to the logarithmic range. Its value is the number of
533 decades to use for each half of the linear range. For example, when
534 *linscale* == 1.0 (the default), the space used for the positive and
535 negative halves of the linear range will be equal to one decade in
536 the logarithmic range.
537 """
538 name = 'symlog'
539 # compatibility shim
540 SymmetricalLogTransform = SymmetricalLogTransform
541 InvertedSymmetricalLogTransform = InvertedSymmetricalLogTransform
543 def __init__(self, axis, **kwargs):
544 if axis.axis_name == 'x':
545 base = kwargs.pop('basex', 10.0)
546 linthresh = kwargs.pop('linthreshx', 2.0)
547 subs = kwargs.pop('subsx', None)
548 linscale = kwargs.pop('linscalex', 1.0)
549 else:
550 base = kwargs.pop('basey', 10.0)
551 linthresh = kwargs.pop('linthreshy', 2.0)
552 subs = kwargs.pop('subsy', None)
553 linscale = kwargs.pop('linscaley', 1.0)
554 if kwargs:
555 warn_deprecated(
556 '3.2.0',
557 message=(
558 f"SymmetricalLogScale got an unexpected keyword "
559 f"argument {next(iter(kwargs))!r}. "
560 'In the future this will raise TypeError')
561 )
562 # raise TypeError(f"SymmetricalLogScale got an unexpected keyword "
563 # f"argument {next(iter(kwargs))!r}")
565 if base <= 1.0:
566 raise ValueError("'basex/basey' must be larger than 1")
567 if linthresh <= 0.0:
568 raise ValueError("'linthreshx/linthreshy' must be positive")
569 if linscale <= 0.0:
570 raise ValueError("'linscalex/linthreshy' must be positive")
572 self._transform = SymmetricalLogTransform(base, linthresh, linscale)
573 self.base = base
574 self.linthresh = linthresh
575 self.linscale = linscale
576 self.subs = subs
578 def set_default_locators_and_formatters(self, axis):
579 # docstring inherited
580 axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
581 axis.set_major_formatter(LogFormatterSciNotation(self.base))
582 axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
583 self.subs))
584 axis.set_minor_formatter(NullFormatter())
586 def get_transform(self):
587 """Return the `.SymmetricalLogTransform` associated with this scale."""
588 return self._transform
591class LogitTransform(Transform):
592 input_dims = output_dims = 1
594 def __init__(self, nonpos='mask'):
595 Transform.__init__(self)
596 cbook._check_in_list(['mask', 'clip'], nonpos=nonpos)
597 self._nonpos = nonpos
598 self._clip = {"clip": True, "mask": False}[nonpos]
600 def transform_non_affine(self, a):
601 """logit transform (base 10), masked or clipped"""
602 with np.errstate(divide="ignore", invalid="ignore"):
603 out = np.log10(a / (1 - a))
604 if self._clip: # See LogTransform for choice of clip value.
605 out[a <= 0] = -1000
606 out[1 <= a] = 1000
607 return out
609 def inverted(self):
610 return LogisticTransform(self._nonpos)
612 def __str__(self):
613 return "{}({!r})".format(type(self).__name__, self._nonpos)
616class LogisticTransform(Transform):
617 input_dims = output_dims = 1
619 def __init__(self, nonpos='mask'):
620 Transform.__init__(self)
621 self._nonpos = nonpos
623 def transform_non_affine(self, a):
624 """logistic transform (base 10)"""
625 return 1.0 / (1 + 10**(-a))
627 def inverted(self):
628 return LogitTransform(self._nonpos)
630 def __str__(self):
631 return "{}({!r})".format(type(self).__name__, self._nonpos)
634class LogitScale(ScaleBase):
635 """
636 Logit scale for data between zero and one, both excluded.
638 This scale is similar to a log scale close to zero and to one, and almost
639 linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
640 """
641 name = 'logit'
643 def __init__(
644 self,
645 axis,
646 nonpos='mask',
647 *,
648 one_half=r"\frac{1}{2}",
649 use_overline=False,
650 ):
651 r"""
652 Parameters
653 ----------
654 axis : `matplotlib.axis.Axis`
655 Currently unused.
656 nonpos : {'mask', 'clip'}
657 Determines the behavior for values beyond the open interval ]0, 1[.
658 They can either be masked as invalid, or clipped to a number very
659 close to 0 or 1.
660 use_overline : bool, default: False
661 Indicate the usage of survival notation (\overline{x}) in place of
662 standard notation (1-x) for probability close to one.
663 one_half : str, default: r"\frac{1}{2}"
664 The string used for ticks formatter to represent 1/2.
665 """
666 self._transform = LogitTransform(nonpos)
667 self._use_overline = use_overline
668 self._one_half = one_half
670 def get_transform(self):
671 """Return the `.LogitTransform` associated with this scale."""
672 return self._transform
674 def set_default_locators_and_formatters(self, axis):
675 # docstring inherited
676 # ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
677 axis.set_major_locator(LogitLocator())
678 axis.set_major_formatter(
679 LogitFormatter(
680 one_half=self._one_half,
681 use_overline=self._use_overline
682 )
683 )
684 axis.set_minor_locator(LogitLocator(minor=True))
685 axis.set_minor_formatter(
686 LogitFormatter(
687 minor=True,
688 one_half=self._one_half,
689 use_overline=self._use_overline
690 )
691 )
693 def limit_range_for_scale(self, vmin, vmax, minpos):
694 """
695 Limit the domain to values between 0 and 1 (excluded).
696 """
697 if not np.isfinite(minpos):
698 minpos = 1e-7 # Should rarely (if ever) have a visible effect.
699 return (minpos if vmin <= 0 else vmin,
700 1 - minpos if vmax >= 1 else vmax)
703_scale_mapping = {
704 'linear': LinearScale,
705 'log': LogScale,
706 'symlog': SymmetricalLogScale,
707 'logit': LogitScale,
708 'function': FuncScale,
709 'functionlog': FuncScaleLog,
710 }
713def get_scale_names():
714 """Return the names of the available scales."""
715 return sorted(_scale_mapping)
718def scale_factory(scale, axis, **kwargs):
719 """
720 Return a scale class by name.
722 Parameters
723 ----------
724 scale : {%(names)s}
725 axis : `matplotlib.axis.Axis`
726 """
727 scale = scale.lower()
728 cbook._check_in_list(_scale_mapping, scale=scale)
729 return _scale_mapping[scale](axis, **kwargs)
732if scale_factory.__doc__:
733 scale_factory.__doc__ = scale_factory.__doc__ % {
734 "names": ", ".join(map(repr, get_scale_names()))}
737def register_scale(scale_class):
738 """
739 Register a new kind of scale.
741 Parameters
742 ----------
743 scale_class : subclass of `ScaleBase`
744 The scale to register.
745 """
746 _scale_mapping[scale_class.name] = scale_class
749@cbook.deprecated(
750 '3.1', message='get_scale_docs() is considered private API since '
751 '3.1 and will be removed from the public API in 3.3.')
752def get_scale_docs():
753 """
754 Helper function for generating docstrings related to scales.
755 """
756 return _get_scale_docs()
759def _get_scale_docs():
760 """
761 Helper function for generating docstrings related to scales.
762 """
763 docs = []
764 for name, scale_class in _scale_mapping.items():
765 docs.extend([
766 f" {name!r}",
767 "",
768 textwrap.indent(inspect.getdoc(scale_class.__init__), " " * 8),
769 ""
770 ])
771 return "\n".join(docs)
774docstring.interpd.update(
775 scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]),
776 scale_docs=_get_scale_docs().rstrip(),
777 )