1"""
2Tick locating and formatting
3============================
4
5This module contains classes for configuring tick locating and formatting.
6Generic tick locators and formatters are provided, as well as domain specific
7custom ones.
8
9Although the locators know nothing about major or minor ticks, they are used
10by the Axis class to support major and minor tick locating and formatting.
11
12.. _tick_locating:
13.. _locators:
14
15Tick locating
16-------------
17
18The Locator class is the base class for all tick locators. The locators
19handle autoscaling of the view limits based on the data limits, and the
20choosing of tick locations. A useful semi-automatic tick locator is
21`MultipleLocator`. It is initialized with a base, e.g., 10, and it picks
22axis limits and ticks that are multiples of that base.
23
24The Locator subclasses defined here are:
25
26======================= =======================================================
27`AutoLocator` `MaxNLocator` with simple defaults. This is the default
28 tick locator for most plotting.
29`MaxNLocator` Finds up to a max number of intervals with ticks at
30 nice locations.
31`LinearLocator` Space ticks evenly from min to max.
32`LogLocator` Space ticks logarithmically from min to max.
33`MultipleLocator` Ticks and range are a multiple of base; either integer
34 or float.
35`FixedLocator` Tick locations are fixed.
36`IndexLocator` Locator for index plots (e.g., where
37 ``x = range(len(y))``).
38`NullLocator` No ticks.
39`SymmetricalLogLocator` Locator for use with the symlog norm; works like
40 `LogLocator` for the part outside of the threshold and
41 adds 0 if inside the limits.
42`AsinhLocator` Locator for use with the asinh norm, attempting to
43 space ticks approximately uniformly.
44`LogitLocator` Locator for logit scaling.
45`AutoMinorLocator` Locator for minor ticks when the axis is linear and the
46 major ticks are uniformly spaced. Subdivides the major
47 tick interval into a specified number of minor
48 intervals, defaulting to 4 or 5 depending on the major
49 interval.
50======================= =======================================================
51
52There are a number of locators specialized for date locations - see
53the :mod:`.dates` module.
54
55You can define your own locator by deriving from Locator. You must
56override the ``__call__`` method, which returns a sequence of locations,
57and you will probably want to override the autoscale method to set the
58view limits from the data limits.
59
60If you want to override the default locator, use one of the above or a custom
61locator and pass it to the x- or y-axis instance. The relevant methods are::
62
63 ax.xaxis.set_major_locator(xmajor_locator)
64 ax.xaxis.set_minor_locator(xminor_locator)
65 ax.yaxis.set_major_locator(ymajor_locator)
66 ax.yaxis.set_minor_locator(yminor_locator)
67
68The default minor locator is `NullLocator`, i.e., no minor ticks on by default.
69
70.. note::
71 `Locator` instances should not be used with more than one
72 `~matplotlib.axis.Axis` or `~matplotlib.axes.Axes`. So instead of::
73
74 locator = MultipleLocator(5)
75 ax.xaxis.set_major_locator(locator)
76 ax2.xaxis.set_major_locator(locator)
77
78 do the following instead::
79
80 ax.xaxis.set_major_locator(MultipleLocator(5))
81 ax2.xaxis.set_major_locator(MultipleLocator(5))
82
83.. _formatters:
84
85Tick formatting
86---------------
87
88Tick formatting is controlled by classes derived from Formatter. The formatter
89operates on a single tick value and returns a string to the axis.
90
91========================= =====================================================
92`NullFormatter` No labels on the ticks.
93`FixedFormatter` Set the strings manually for the labels.
94`FuncFormatter` User defined function sets the labels.
95`StrMethodFormatter` Use string `format` method.
96`FormatStrFormatter` Use an old-style sprintf format string.
97`ScalarFormatter` Default formatter for scalars: autopick the format
98 string.
99`LogFormatter` Formatter for log axes.
100`LogFormatterExponent` Format values for log axis using
101 ``exponent = log_base(value)``.
102`LogFormatterMathtext` Format values for log axis using
103 ``exponent = log_base(value)`` using Math text.
104`LogFormatterSciNotation` Format values for log axis using scientific notation.
105`LogitFormatter` Probability formatter.
106`EngFormatter` Format labels in engineering notation.
107`PercentFormatter` Format labels as a percentage.
108========================= =====================================================
109
110You can derive your own formatter from the Formatter base class by
111simply overriding the ``__call__`` method. The formatter class has
112access to the axis view and data limits.
113
114To control the major and minor tick label formats, use one of the
115following methods::
116
117 ax.xaxis.set_major_formatter(xmajor_formatter)
118 ax.xaxis.set_minor_formatter(xminor_formatter)
119 ax.yaxis.set_major_formatter(ymajor_formatter)
120 ax.yaxis.set_minor_formatter(yminor_formatter)
121
122In addition to a `.Formatter` instance, `~.Axis.set_major_formatter` and
123`~.Axis.set_minor_formatter` also accept a ``str`` or function. ``str`` input
124will be internally replaced with an autogenerated `.StrMethodFormatter` with
125the input ``str``. For function input, a `.FuncFormatter` with the input
126function will be generated and used.
127
128See :doc:`/gallery/ticks/major_minor_demo` for an example of setting major
129and minor ticks. See the :mod:`matplotlib.dates` module for more information
130and examples of using date locators and formatters.
131"""
132
133import itertools
134import logging
135import locale
136import math
137from numbers import Integral
138import string
139
140import numpy as np
141
142import matplotlib as mpl
143from matplotlib import _api, cbook
144from matplotlib import transforms as mtransforms
145
146_log = logging.getLogger(__name__)
147
148__all__ = ('TickHelper', 'Formatter', 'FixedFormatter',
149 'NullFormatter', 'FuncFormatter', 'FormatStrFormatter',
150 'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter',
151 'LogFormatterExponent', 'LogFormatterMathtext',
152 'LogFormatterSciNotation',
153 'LogitFormatter', 'EngFormatter', 'PercentFormatter',
154 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator',
155 'LinearLocator', 'LogLocator', 'AutoLocator',
156 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
157 'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator')
158
159
160class _DummyAxis:
161 __name__ = "dummy"
162
163 def __init__(self, minpos=0):
164 self._data_interval = (0, 1)
165 self._view_interval = (0, 1)
166 self._minpos = minpos
167
168 def get_view_interval(self):
169 return self._view_interval
170
171 def set_view_interval(self, vmin, vmax):
172 self._view_interval = (vmin, vmax)
173
174 def get_minpos(self):
175 return self._minpos
176
177 def get_data_interval(self):
178 return self._data_interval
179
180 def set_data_interval(self, vmin, vmax):
181 self._data_interval = (vmin, vmax)
182
183 def get_tick_space(self):
184 # Just use the long-standing default of nbins==9
185 return 9
186
187
188class TickHelper:
189 axis = None
190
191 def set_axis(self, axis):
192 self.axis = axis
193
194 def create_dummy_axis(self, **kwargs):
195 if self.axis is None:
196 self.axis = _DummyAxis(**kwargs)
197
198
199class Formatter(TickHelper):
200 """
201 Create a string based on a tick value and location.
202 """
203 # some classes want to see all the locs to help format
204 # individual ones
205 locs = []
206
207 def __call__(self, x, pos=None):
208 """
209 Return the format for tick value *x* at position pos.
210 ``pos=None`` indicates an unspecified location.
211 """
212 raise NotImplementedError('Derived must override')
213
214 def format_ticks(self, values):
215 """Return the tick labels for all the ticks at once."""
216 self.set_locs(values)
217 return [self(value, i) for i, value in enumerate(values)]
218
219 def format_data(self, value):
220 """
221 Return the full string representation of the value with the
222 position unspecified.
223 """
224 return self.__call__(value)
225
226 def format_data_short(self, value):
227 """
228 Return a short string version of the tick value.
229
230 Defaults to the position-independent long value.
231 """
232 return self.format_data(value)
233
234 def get_offset(self):
235 return ''
236
237 def set_locs(self, locs):
238 """
239 Set the locations of the ticks.
240
241 This method is called before computing the tick labels because some
242 formatters need to know all tick locations to do so.
243 """
244 self.locs = locs
245
246 @staticmethod
247 def fix_minus(s):
248 """
249 Some classes may want to replace a hyphen for minus with the proper
250 Unicode symbol (U+2212) for typographical correctness. This is a
251 helper method to perform such a replacement when it is enabled via
252 :rc:`axes.unicode_minus`.
253 """
254 return (s.replace('-', '\N{MINUS SIGN}')
255 if mpl.rcParams['axes.unicode_minus']
256 else s)
257
258 def _set_locator(self, locator):
259 """Subclasses may want to override this to set a locator."""
260 pass
261
262
263class NullFormatter(Formatter):
264 """Always return the empty string."""
265
266 def __call__(self, x, pos=None):
267 # docstring inherited
268 return ''
269
270
271class FixedFormatter(Formatter):
272 """
273 Return fixed strings for tick labels based only on position, not value.
274
275 .. note::
276 `.FixedFormatter` should only be used together with `.FixedLocator`.
277 Otherwise, the labels may end up in unexpected positions.
278 """
279
280 def __init__(self, seq):
281 """Set the sequence *seq* of strings that will be used for labels."""
282 self.seq = seq
283 self.offset_string = ''
284
285 def __call__(self, x, pos=None):
286 """
287 Return the label that matches the position, regardless of the value.
288
289 For positions ``pos < len(seq)``, return ``seq[i]`` regardless of
290 *x*. Otherwise return empty string. ``seq`` is the sequence of
291 strings that this object was initialized with.
292 """
293 if pos is None or pos >= len(self.seq):
294 return ''
295 else:
296 return self.seq[pos]
297
298 def get_offset(self):
299 return self.offset_string
300
301 def set_offset_string(self, ofs):
302 self.offset_string = ofs
303
304
305class FuncFormatter(Formatter):
306 """
307 Use a user-defined function for formatting.
308
309 The function should take in two inputs (a tick value ``x`` and a
310 position ``pos``), and return a string containing the corresponding
311 tick label.
312 """
313
314 def __init__(self, func):
315 self.func = func
316 self.offset_string = ""
317
318 def __call__(self, x, pos=None):
319 """
320 Return the value of the user defined function.
321
322 *x* and *pos* are passed through as-is.
323 """
324 return self.func(x, pos)
325
326 def get_offset(self):
327 return self.offset_string
328
329 def set_offset_string(self, ofs):
330 self.offset_string = ofs
331
332
333class FormatStrFormatter(Formatter):
334 """
335 Use an old-style ('%' operator) format string to format the tick.
336
337 The format string should have a single variable format (%) in it.
338 It will be applied to the value (not the position) of the tick.
339
340 Negative numeric values (e.g., -1) will use a dash, not a Unicode minus;
341 use mathtext to get a Unicode minus by wrapping the format specifier with $
342 (e.g. "$%g$").
343 """
344
345 def __init__(self, fmt):
346 self.fmt = fmt
347
348 def __call__(self, x, pos=None):
349 """
350 Return the formatted label string.
351
352 Only the value *x* is formatted. The position is ignored.
353 """
354 return self.fmt % x
355
356
357class _UnicodeMinusFormat(string.Formatter):
358 """
359 A specialized string formatter so that `.StrMethodFormatter` respects
360 :rc:`axes.unicode_minus`. This implementation relies on the fact that the
361 format string is only ever called with kwargs *x* and *pos*, so it blindly
362 replaces dashes by unicode minuses without further checking.
363 """
364
365 def format_field(self, value, format_spec):
366 return Formatter.fix_minus(super().format_field(value, format_spec))
367
368
369class StrMethodFormatter(Formatter):
370 """
371 Use a new-style format string (as used by `str.format`) to format the tick.
372
373 The field used for the tick value must be labeled *x* and the field used
374 for the tick position must be labeled *pos*.
375
376 The formatter will respect :rc:`axes.unicode_minus` when formatting
377 negative numeric values.
378
379 It is typically unnecessary to explicitly construct `.StrMethodFormatter`
380 objects, as `~.Axis.set_major_formatter` directly accepts the format string
381 itself.
382 """
383
384 def __init__(self, fmt):
385 self.fmt = fmt
386
387 def __call__(self, x, pos=None):
388 """
389 Return the formatted label string.
390
391 *x* and *pos* are passed to `str.format` as keyword arguments
392 with those exact names.
393 """
394 return _UnicodeMinusFormat().format(self.fmt, x=x, pos=pos)
395
396
397class ScalarFormatter(Formatter):
398 """
399 Format tick values as a number.
400
401 Parameters
402 ----------
403 useOffset : bool or float, default: :rc:`axes.formatter.useoffset`
404 Whether to use offset notation. See `.set_useOffset`.
405 useMathText : bool, default: :rc:`axes.formatter.use_mathtext`
406 Whether to use fancy math formatting. See `.set_useMathText`.
407 useLocale : bool, default: :rc:`axes.formatter.use_locale`.
408 Whether to use locale settings for decimal sign and positive sign.
409 See `.set_useLocale`.
410
411 Notes
412 -----
413 In addition to the parameters above, the formatting of scientific vs.
414 floating point representation can be configured via `.set_scientific`
415 and `.set_powerlimits`).
416
417 **Offset notation and scientific notation**
418
419 Offset notation and scientific notation look quite similar at first sight.
420 Both split some information from the formatted tick values and display it
421 at the end of the axis.
422
423 - The scientific notation splits up the order of magnitude, i.e. a
424 multiplicative scaling factor, e.g. ``1e6``.
425
426 - The offset notation separates an additive constant, e.g. ``+1e6``. The
427 offset notation label is always prefixed with a ``+`` or ``-`` sign
428 and is thus distinguishable from the order of magnitude label.
429
430 The following plot with x limits ``1_000_000`` to ``1_000_010`` illustrates
431 the different formatting. Note the labels at the right edge of the x axis.
432
433 .. plot::
434
435 lim = (1_000_000, 1_000_010)
436
437 fig, (ax1, ax2, ax3) = plt.subplots(3, 1, gridspec_kw={'hspace': 2})
438 ax1.set(title='offset notation', xlim=lim)
439 ax2.set(title='scientific notation', xlim=lim)
440 ax2.xaxis.get_major_formatter().set_useOffset(False)
441 ax3.set(title='floating-point notation', xlim=lim)
442 ax3.xaxis.get_major_formatter().set_useOffset(False)
443 ax3.xaxis.get_major_formatter().set_scientific(False)
444
445 """
446
447 def __init__(self, useOffset=None, useMathText=None, useLocale=None):
448 if useOffset is None:
449 useOffset = mpl.rcParams['axes.formatter.useoffset']
450 self._offset_threshold = \
451 mpl.rcParams['axes.formatter.offset_threshold']
452 self.set_useOffset(useOffset)
453 self._usetex = mpl.rcParams['text.usetex']
454 self.set_useMathText(useMathText)
455 self.orderOfMagnitude = 0
456 self.format = ''
457 self._scientific = True
458 self._powerlimits = mpl.rcParams['axes.formatter.limits']
459 self.set_useLocale(useLocale)
460
461 def get_useOffset(self):
462 """
463 Return whether automatic mode for offset notation is active.
464
465 This returns True if ``set_useOffset(True)``; it returns False if an
466 explicit offset was set, e.g. ``set_useOffset(1000)``.
467
468 See Also
469 --------
470 ScalarFormatter.set_useOffset
471 """
472 return self._useOffset
473
474 def set_useOffset(self, val):
475 """
476 Set whether to use offset notation.
477
478 When formatting a set numbers whose value is large compared to their
479 range, the formatter can separate an additive constant. This can
480 shorten the formatted numbers so that they are less likely to overlap
481 when drawn on an axis.
482
483 Parameters
484 ----------
485 val : bool or float
486 - If False, do not use offset notation.
487 - If True (=automatic mode), use offset notation if it can make
488 the residual numbers significantly shorter. The exact behavior
489 is controlled by :rc:`axes.formatter.offset_threshold`.
490 - If a number, force an offset of the given value.
491
492 Examples
493 --------
494 With active offset notation, the values
495
496 ``100_000, 100_002, 100_004, 100_006, 100_008``
497
498 will be formatted as ``0, 2, 4, 6, 8`` plus an offset ``+1e5``, which
499 is written to the edge of the axis.
500 """
501 if val in [True, False]:
502 self.offset = 0
503 self._useOffset = val
504 else:
505 self._useOffset = False
506 self.offset = val
507
508 useOffset = property(fget=get_useOffset, fset=set_useOffset)
509
510 def get_useLocale(self):
511 """
512 Return whether locale settings are used for formatting.
513
514 See Also
515 --------
516 ScalarFormatter.set_useLocale
517 """
518 return self._useLocale
519
520 def set_useLocale(self, val):
521 """
522 Set whether to use locale settings for decimal sign and positive sign.
523
524 Parameters
525 ----------
526 val : bool or None
527 *None* resets to :rc:`axes.formatter.use_locale`.
528 """
529 if val is None:
530 self._useLocale = mpl.rcParams['axes.formatter.use_locale']
531 else:
532 self._useLocale = val
533
534 useLocale = property(fget=get_useLocale, fset=set_useLocale)
535
536 def _format_maybe_minus_and_locale(self, fmt, arg):
537 """
538 Format *arg* with *fmt*, applying Unicode minus and locale if desired.
539 """
540 return self.fix_minus(
541 # Escape commas introduced by locale.format_string if using math text,
542 # but not those present from the beginning in fmt.
543 (",".join(locale.format_string(part, (arg,), True).replace(",", "{,}")
544 for part in fmt.split(",")) if self._useMathText
545 else locale.format_string(fmt, (arg,), True))
546 if self._useLocale
547 else fmt % arg)
548
549 def get_useMathText(self):
550 """
551 Return whether to use fancy math formatting.
552
553 See Also
554 --------
555 ScalarFormatter.set_useMathText
556 """
557 return self._useMathText
558
559 def set_useMathText(self, val):
560 r"""
561 Set whether to use fancy math formatting.
562
563 If active, scientific notation is formatted as :math:`1.2 \times 10^3`.
564
565 Parameters
566 ----------
567 val : bool or None
568 *None* resets to :rc:`axes.formatter.use_mathtext`.
569 """
570 if val is None:
571 self._useMathText = mpl.rcParams['axes.formatter.use_mathtext']
572 if self._useMathText is False:
573 try:
574 from matplotlib import font_manager
575 ufont = font_manager.findfont(
576 font_manager.FontProperties(
577 mpl.rcParams["font.family"]
578 ),
579 fallback_to_default=False,
580 )
581 except ValueError:
582 ufont = None
583
584 if ufont == str(cbook._get_data_path("fonts/ttf/cmr10.ttf")):
585 _api.warn_external(
586 "cmr10 font should ideally be used with "
587 "mathtext, set axes.formatter.use_mathtext to True"
588 )
589 else:
590 self._useMathText = val
591
592 useMathText = property(fget=get_useMathText, fset=set_useMathText)
593
594 def __call__(self, x, pos=None):
595 """
596 Return the format for tick value *x* at position *pos*.
597 """
598 if len(self.locs) == 0:
599 return ''
600 else:
601 xp = (x - self.offset) / (10. ** self.orderOfMagnitude)
602 if abs(xp) < 1e-8:
603 xp = 0
604 return self._format_maybe_minus_and_locale(self.format, xp)
605
606 def set_scientific(self, b):
607 """
608 Turn scientific notation on or off.
609
610 See Also
611 --------
612 ScalarFormatter.set_powerlimits
613 """
614 self._scientific = bool(b)
615
616 def set_powerlimits(self, lims):
617 r"""
618 Set size thresholds for scientific notation.
619
620 Parameters
621 ----------
622 lims : (int, int)
623 A tuple *(min_exp, max_exp)* containing the powers of 10 that
624 determine the switchover threshold. For a number representable as
625 :math:`a \times 10^\mathrm{exp}` with :math:`1 <= |a| < 10`,
626 scientific notation will be used if ``exp <= min_exp`` or
627 ``exp >= max_exp``.
628
629 The default limits are controlled by :rc:`axes.formatter.limits`.
630
631 In particular numbers with *exp* equal to the thresholds are
632 written in scientific notation.
633
634 Typically, *min_exp* will be negative and *max_exp* will be
635 positive.
636
637 For example, ``formatter.set_powerlimits((-3, 4))`` will provide
638 the following formatting:
639 :math:`1 \times 10^{-3}, 9.9 \times 10^{-3}, 0.01,`
640 :math:`9999, 1 \times 10^4`.
641
642 See Also
643 --------
644 ScalarFormatter.set_scientific
645 """
646 if len(lims) != 2:
647 raise ValueError("'lims' must be a sequence of length 2")
648 self._powerlimits = lims
649
650 def format_data_short(self, value):
651 # docstring inherited
652 if value is np.ma.masked:
653 return ""
654 if isinstance(value, Integral):
655 fmt = "%d"
656 else:
657 if getattr(self.axis, "__name__", "") in ["xaxis", "yaxis"]:
658 if self.axis.__name__ == "xaxis":
659 axis_trf = self.axis.axes.get_xaxis_transform()
660 axis_inv_trf = axis_trf.inverted()
661 screen_xy = axis_trf.transform((value, 0))
662 neighbor_values = axis_inv_trf.transform(
663 screen_xy + [[-1, 0], [+1, 0]])[:, 0]
664 else: # yaxis:
665 axis_trf = self.axis.axes.get_yaxis_transform()
666 axis_inv_trf = axis_trf.inverted()
667 screen_xy = axis_trf.transform((0, value))
668 neighbor_values = axis_inv_trf.transform(
669 screen_xy + [[0, -1], [0, +1]])[:, 1]
670 delta = abs(neighbor_values - value).max()
671 else:
672 # Rough approximation: no more than 1e4 divisions.
673 a, b = self.axis.get_view_interval()
674 delta = (b - a) / 1e4
675 fmt = f"%-#.{cbook._g_sig_digits(value, delta)}g"
676 return self._format_maybe_minus_and_locale(fmt, value)
677
678 def format_data(self, value):
679 # docstring inherited
680 e = math.floor(math.log10(abs(value)))
681 s = round(value / 10**e, 10)
682 significand = self._format_maybe_minus_and_locale(
683 "%d" if s % 1 == 0 else "%1.10g", s)
684 if e == 0:
685 return significand
686 exponent = self._format_maybe_minus_and_locale("%d", e)
687 if self._useMathText or self._usetex:
688 exponent = "10^{%s}" % exponent
689 return (exponent if s == 1 # reformat 1x10^y as 10^y
690 else rf"{significand} \times {exponent}")
691 else:
692 return f"{significand}e{exponent}"
693
694 def get_offset(self):
695 """
696 Return scientific notation, plus offset.
697 """
698 if len(self.locs) == 0:
699 return ''
700 if self.orderOfMagnitude or self.offset:
701 offsetStr = ''
702 sciNotStr = ''
703 if self.offset:
704 offsetStr = self.format_data(self.offset)
705 if self.offset > 0:
706 offsetStr = '+' + offsetStr
707 if self.orderOfMagnitude:
708 if self._usetex or self._useMathText:
709 sciNotStr = self.format_data(10 ** self.orderOfMagnitude)
710 else:
711 sciNotStr = '1e%d' % self.orderOfMagnitude
712 if self._useMathText or self._usetex:
713 if sciNotStr != '':
714 sciNotStr = r'\times\mathdefault{%s}' % sciNotStr
715 s = fr'${sciNotStr}\mathdefault{{{offsetStr}}}$'
716 else:
717 s = ''.join((sciNotStr, offsetStr))
718 return self.fix_minus(s)
719 return ''
720
721 def set_locs(self, locs):
722 # docstring inherited
723 self.locs = locs
724 if len(self.locs) > 0:
725 if self._useOffset:
726 self._compute_offset()
727 self._set_order_of_magnitude()
728 self._set_format()
729
730 def _compute_offset(self):
731 locs = self.locs
732 # Restrict to visible ticks.
733 vmin, vmax = sorted(self.axis.get_view_interval())
734 locs = np.asarray(locs)
735 locs = locs[(vmin <= locs) & (locs <= vmax)]
736 if not len(locs):
737 self.offset = 0
738 return
739 lmin, lmax = locs.min(), locs.max()
740 # Only use offset if there are at least two ticks and every tick has
741 # the same sign.
742 if lmin == lmax or lmin <= 0 <= lmax:
743 self.offset = 0
744 return
745 # min, max comparing absolute values (we want division to round towards
746 # zero so we work on absolute values).
747 abs_min, abs_max = sorted([abs(float(lmin)), abs(float(lmax))])
748 sign = math.copysign(1, lmin)
749 # What is the smallest power of ten such that abs_min and abs_max are
750 # equal up to that precision?
751 # Note: Internally using oom instead of 10 ** oom avoids some numerical
752 # accuracy issues.
753 oom_max = np.ceil(math.log10(abs_max))
754 oom = 1 + next(oom for oom in itertools.count(oom_max, -1)
755 if abs_min // 10 ** oom != abs_max // 10 ** oom)
756 if (abs_max - abs_min) / 10 ** oom <= 1e-2:
757 # Handle the case of straddling a multiple of a large power of ten
758 # (relative to the span).
759 # What is the smallest power of ten such that abs_min and abs_max
760 # are no more than 1 apart at that precision?
761 oom = 1 + next(oom for oom in itertools.count(oom_max, -1)
762 if abs_max // 10 ** oom - abs_min // 10 ** oom > 1)
763 # Only use offset if it saves at least _offset_threshold digits.
764 n = self._offset_threshold - 1
765 self.offset = (sign * (abs_max // 10 ** oom) * 10 ** oom
766 if abs_max // 10 ** oom >= 10**n
767 else 0)
768
769 def _set_order_of_magnitude(self):
770 # if scientific notation is to be used, find the appropriate exponent
771 # if using a numerical offset, find the exponent after applying the
772 # offset. When lower power limit = upper <> 0, use provided exponent.
773 if not self._scientific:
774 self.orderOfMagnitude = 0
775 return
776 if self._powerlimits[0] == self._powerlimits[1] != 0:
777 # fixed scaling when lower power limit = upper <> 0.
778 self.orderOfMagnitude = self._powerlimits[0]
779 return
780 # restrict to visible ticks
781 vmin, vmax = sorted(self.axis.get_view_interval())
782 locs = np.asarray(self.locs)
783 locs = locs[(vmin <= locs) & (locs <= vmax)]
784 locs = np.abs(locs)
785 if not len(locs):
786 self.orderOfMagnitude = 0
787 return
788 if self.offset:
789 oom = math.floor(math.log10(vmax - vmin))
790 else:
791 val = locs.max()
792 if val == 0:
793 oom = 0
794 else:
795 oom = math.floor(math.log10(val))
796 if oom <= self._powerlimits[0]:
797 self.orderOfMagnitude = oom
798 elif oom >= self._powerlimits[1]:
799 self.orderOfMagnitude = oom
800 else:
801 self.orderOfMagnitude = 0
802
803 def _set_format(self):
804 # set the format string to format all the ticklabels
805 if len(self.locs) < 2:
806 # Temporarily augment the locations with the axis end points.
807 _locs = [*self.locs, *self.axis.get_view_interval()]
808 else:
809 _locs = self.locs
810 locs = (np.asarray(_locs) - self.offset) / 10. ** self.orderOfMagnitude
811 loc_range = np.ptp(locs)
812 # Curvilinear coordinates can yield two identical points.
813 if loc_range == 0:
814 loc_range = np.max(np.abs(locs))
815 # Both points might be zero.
816 if loc_range == 0:
817 loc_range = 1
818 if len(self.locs) < 2:
819 # We needed the end points only for the loc_range calculation.
820 locs = locs[:-2]
821 loc_range_oom = int(math.floor(math.log10(loc_range)))
822 # first estimate:
823 sigfigs = max(0, 3 - loc_range_oom)
824 # refined estimate:
825 thresh = 1e-3 * 10 ** loc_range_oom
826 while sigfigs >= 0:
827 if np.abs(locs - np.round(locs, decimals=sigfigs)).max() < thresh:
828 sigfigs -= 1
829 else:
830 break
831 sigfigs += 1
832 self.format = f'%1.{sigfigs}f'
833 if self._usetex or self._useMathText:
834 self.format = r'$\mathdefault{%s}$' % self.format
835
836
837class LogFormatter(Formatter):
838 """
839 Base class for formatting ticks on a log or symlog scale.
840
841 It may be instantiated directly, or subclassed.
842
843 Parameters
844 ----------
845 base : float, default: 10.
846 Base of the logarithm used in all calculations.
847
848 labelOnlyBase : bool, default: False
849 If True, label ticks only at integer powers of base.
850 This is normally True for major ticks and False for
851 minor ticks.
852
853 minor_thresholds : (subset, all), default: (1, 0.4)
854 If labelOnlyBase is False, these two numbers control
855 the labeling of ticks that are not at integer powers of
856 base; normally these are the minor ticks. The controlling
857 parameter is the log of the axis data range. In the typical
858 case where base is 10 it is the number of decades spanned
859 by the axis, so we can call it 'numdec'. If ``numdec <= all``,
860 all minor ticks will be labeled. If ``all < numdec <= subset``,
861 then only a subset of minor ticks will be labeled, so as to
862 avoid crowding. If ``numdec > subset`` then no minor ticks will
863 be labeled.
864
865 linthresh : None or float, default: None
866 If a symmetric log scale is in use, its ``linthresh``
867 parameter must be supplied here.
868
869 Notes
870 -----
871 The `set_locs` method must be called to enable the subsetting
872 logic controlled by the ``minor_thresholds`` parameter.
873
874 In some cases such as the colorbar, there is no distinction between
875 major and minor ticks; the tick locations might be set manually,
876 or by a locator that puts ticks at integer powers of base and
877 at intermediate locations. For this situation, disable the
878 minor_thresholds logic by using ``minor_thresholds=(np.inf, np.inf)``,
879 so that all ticks will be labeled.
880
881 To disable labeling of minor ticks when 'labelOnlyBase' is False,
882 use ``minor_thresholds=(0, 0)``. This is the default for the
883 "classic" style.
884
885 Examples
886 --------
887 To label a subset of minor ticks when the view limits span up
888 to 2 decades, and all of the ticks when zoomed in to 0.5 decades
889 or less, use ``minor_thresholds=(2, 0.5)``.
890
891 To label all minor ticks when the view limits span up to 1.5
892 decades, use ``minor_thresholds=(1.5, 1.5)``.
893 """
894
895 def __init__(self, base=10.0, labelOnlyBase=False,
896 minor_thresholds=None,
897 linthresh=None):
898
899 self.set_base(base)
900 self.set_label_minor(labelOnlyBase)
901 if minor_thresholds is None:
902 if mpl.rcParams['_internal.classic_mode']:
903 minor_thresholds = (0, 0)
904 else:
905 minor_thresholds = (1, 0.4)
906 self.minor_thresholds = minor_thresholds
907 self._sublabels = None
908 self._linthresh = linthresh
909
910 def set_base(self, base):
911 """
912 Change the *base* for labeling.
913
914 .. warning::
915 Should always match the base used for :class:`LogLocator`
916 """
917 self._base = float(base)
918
919 def set_label_minor(self, labelOnlyBase):
920 """
921 Switch minor tick labeling on or off.
922
923 Parameters
924 ----------
925 labelOnlyBase : bool
926 If True, label ticks only at integer powers of base.
927 """
928 self.labelOnlyBase = labelOnlyBase
929
930 def set_locs(self, locs=None):
931 """
932 Use axis view limits to control which ticks are labeled.
933
934 The *locs* parameter is ignored in the present algorithm.
935 """
936 if np.isinf(self.minor_thresholds[0]):
937 self._sublabels = None
938 return
939
940 # Handle symlog case:
941 linthresh = self._linthresh
942 if linthresh is None:
943 try:
944 linthresh = self.axis.get_transform().linthresh
945 except AttributeError:
946 pass
947
948 vmin, vmax = self.axis.get_view_interval()
949 if vmin > vmax:
950 vmin, vmax = vmax, vmin
951
952 if linthresh is None and vmin <= 0:
953 # It's probably a colorbar with
954 # a format kwarg setting a LogFormatter in the manner
955 # that worked with 1.5.x, but that doesn't work now.
956 self._sublabels = {1} # label powers of base
957 return
958
959 b = self._base
960 if linthresh is not None: # symlog
961 # Only compute the number of decades in the logarithmic part of the
962 # axis
963 numdec = 0
964 if vmin < -linthresh:
965 rhs = min(vmax, -linthresh)
966 numdec += math.log(vmin / rhs) / math.log(b)
967 if vmax > linthresh:
968 lhs = max(vmin, linthresh)
969 numdec += math.log(vmax / lhs) / math.log(b)
970 else:
971 vmin = math.log(vmin) / math.log(b)
972 vmax = math.log(vmax) / math.log(b)
973 numdec = abs(vmax - vmin)
974
975 if numdec > self.minor_thresholds[0]:
976 # Label only bases
977 self._sublabels = {1}
978 elif numdec > self.minor_thresholds[1]:
979 # Add labels between bases at log-spaced coefficients;
980 # include base powers in case the locations include
981 # "major" and "minor" points, as in colorbar.
982 c = np.geomspace(1, b, int(b)//2 + 1)
983 self._sublabels = set(np.round(c))
984 # For base 10, this yields (1, 2, 3, 4, 6, 10).
985 else:
986 # Label all integer multiples of base**n.
987 self._sublabels = set(np.arange(1, b + 1))
988
989 def _num_to_string(self, x, vmin, vmax):
990 if x > 10000:
991 s = '%1.0e' % x
992 elif x < 1:
993 s = '%1.0e' % x
994 else:
995 s = self._pprint_val(x, vmax - vmin)
996 return s
997
998 def __call__(self, x, pos=None):
999 # docstring inherited
1000 if x == 0.0: # Symlog
1001 return '0'
1002
1003 x = abs(x)
1004 b = self._base
1005 # only label the decades
1006 fx = math.log(x) / math.log(b)
1007 is_x_decade = _is_close_to_int(fx)
1008 exponent = round(fx) if is_x_decade else np.floor(fx)
1009 coeff = round(b ** (fx - exponent))
1010
1011 if self.labelOnlyBase and not is_x_decade:
1012 return ''
1013 if self._sublabels is not None and coeff not in self._sublabels:
1014 return ''
1015
1016 vmin, vmax = self.axis.get_view_interval()
1017 vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
1018 s = self._num_to_string(x, vmin, vmax)
1019 return self.fix_minus(s)
1020
1021 def format_data(self, value):
1022 with cbook._setattr_cm(self, labelOnlyBase=False):
1023 return cbook.strip_math(self.__call__(value))
1024
1025 def format_data_short(self, value):
1026 # docstring inherited
1027 return ('%-12g' % value).rstrip()
1028
1029 def _pprint_val(self, x, d):
1030 # If the number is not too big and it's an int, format it as an int.
1031 if abs(x) < 1e4 and x == int(x):
1032 return '%d' % x
1033 fmt = ('%1.3e' if d < 1e-2 else
1034 '%1.3f' if d <= 1 else
1035 '%1.2f' if d <= 10 else
1036 '%1.1f' if d <= 1e5 else
1037 '%1.1e')
1038 s = fmt % x
1039 tup = s.split('e')
1040 if len(tup) == 2:
1041 mantissa = tup[0].rstrip('0').rstrip('.')
1042 exponent = int(tup[1])
1043 if exponent:
1044 s = '%se%d' % (mantissa, exponent)
1045 else:
1046 s = mantissa
1047 else:
1048 s = s.rstrip('0').rstrip('.')
1049 return s
1050
1051
1052class LogFormatterExponent(LogFormatter):
1053 """
1054 Format values for log axis using ``exponent = log_base(value)``.
1055 """
1056 def _num_to_string(self, x, vmin, vmax):
1057 fx = math.log(x) / math.log(self._base)
1058 if abs(fx) > 10000:
1059 s = '%1.0g' % fx
1060 elif abs(fx) < 1:
1061 s = '%1.0g' % fx
1062 else:
1063 fd = math.log(vmax - vmin) / math.log(self._base)
1064 s = self._pprint_val(fx, fd)
1065 return s
1066
1067
1068class LogFormatterMathtext(LogFormatter):
1069 """
1070 Format values for log axis using ``exponent = log_base(value)``.
1071 """
1072
1073 def _non_decade_format(self, sign_string, base, fx, usetex):
1074 """Return string for non-decade locations."""
1075 return r'$\mathdefault{%s%s^{%.2f}}$' % (sign_string, base, fx)
1076
1077 def __call__(self, x, pos=None):
1078 # docstring inherited
1079 if x == 0: # Symlog
1080 return r'$\mathdefault{0}$'
1081
1082 sign_string = '-' if x < 0 else ''
1083 x = abs(x)
1084 b = self._base
1085
1086 # only label the decades
1087 fx = math.log(x) / math.log(b)
1088 is_x_decade = _is_close_to_int(fx)
1089 exponent = round(fx) if is_x_decade else np.floor(fx)
1090 coeff = round(b ** (fx - exponent))
1091
1092 if self.labelOnlyBase and not is_x_decade:
1093 return ''
1094 if self._sublabels is not None and coeff not in self._sublabels:
1095 return ''
1096
1097 if is_x_decade:
1098 fx = round(fx)
1099
1100 # use string formatting of the base if it is not an integer
1101 if b % 1 == 0.0:
1102 base = '%d' % b
1103 else:
1104 base = '%s' % b
1105
1106 if abs(fx) < mpl.rcParams['axes.formatter.min_exponent']:
1107 return r'$\mathdefault{%s%g}$' % (sign_string, x)
1108 elif not is_x_decade:
1109 usetex = mpl.rcParams['text.usetex']
1110 return self._non_decade_format(sign_string, base, fx, usetex)
1111 else:
1112 return r'$\mathdefault{%s%s^{%d}}$' % (sign_string, base, fx)
1113
1114
1115class LogFormatterSciNotation(LogFormatterMathtext):
1116 """
1117 Format values following scientific notation in a logarithmic axis.
1118 """
1119
1120 def _non_decade_format(self, sign_string, base, fx, usetex):
1121 """Return string for non-decade locations."""
1122 b = float(base)
1123 exponent = math.floor(fx)
1124 coeff = b ** (fx - exponent)
1125 if _is_close_to_int(coeff):
1126 coeff = round(coeff)
1127 return r'$\mathdefault{%s%g\times%s^{%d}}$' \
1128 % (sign_string, coeff, base, exponent)
1129
1130
1131class LogitFormatter(Formatter):
1132 """
1133 Probability formatter (using Math text).
1134 """
1135
1136 def __init__(
1137 self,
1138 *,
1139 use_overline=False,
1140 one_half=r"\frac{1}{2}",
1141 minor=False,
1142 minor_threshold=25,
1143 minor_number=6,
1144 ):
1145 r"""
1146 Parameters
1147 ----------
1148 use_overline : bool, default: False
1149 If x > 1/2, with x = 1 - v, indicate if x should be displayed as
1150 $\overline{v}$. The default is to display $1 - v$.
1151
1152 one_half : str, default: r"\\frac{1}{2}"
1153 The string used to represent 1/2.
1154
1155 minor : bool, default: False
1156 Indicate if the formatter is formatting minor ticks or not.
1157 Basically minor ticks are not labelled, except when only few ticks
1158 are provided, ticks with most space with neighbor ticks are
1159 labelled. See other parameters to change the default behavior.
1160
1161 minor_threshold : int, default: 25
1162 Maximum number of locs for labelling some minor ticks. This
1163 parameter have no effect if minor is False.
1164
1165 minor_number : int, default: 6
1166 Number of ticks which are labelled when the number of ticks is
1167 below the threshold.
1168 """
1169 self._use_overline = use_overline
1170 self._one_half = one_half
1171 self._minor = minor
1172 self._labelled = set()
1173 self._minor_threshold = minor_threshold
1174 self._minor_number = minor_number
1175
1176 def use_overline(self, use_overline):
1177 r"""
1178 Switch display mode with overline for labelling p>1/2.
1179
1180 Parameters
1181 ----------
1182 use_overline : bool
1183 If x > 1/2, with x = 1 - v, indicate if x should be displayed as
1184 $\overline{v}$. The default is to display $1 - v$.
1185 """
1186 self._use_overline = use_overline
1187
1188 def set_one_half(self, one_half):
1189 r"""
1190 Set the way one half is displayed.
1191
1192 one_half : str
1193 The string used to represent 1/2.
1194 """
1195 self._one_half = one_half
1196
1197 def set_minor_threshold(self, minor_threshold):
1198 """
1199 Set the threshold for labelling minors ticks.
1200
1201 Parameters
1202 ----------
1203 minor_threshold : int
1204 Maximum number of locations for labelling some minor ticks. This
1205 parameter have no effect if minor is False.
1206 """
1207 self._minor_threshold = minor_threshold
1208
1209 def set_minor_number(self, minor_number):
1210 """
1211 Set the number of minor ticks to label when some minor ticks are
1212 labelled.
1213
1214 Parameters
1215 ----------
1216 minor_number : int
1217 Number of ticks which are labelled when the number of ticks is
1218 below the threshold.
1219 """
1220 self._minor_number = minor_number
1221
1222 def set_locs(self, locs):
1223 self.locs = np.array(locs)
1224 self._labelled.clear()
1225
1226 if not self._minor:
1227 return None
1228 if all(
1229 _is_decade(x, rtol=1e-7)
1230 or _is_decade(1 - x, rtol=1e-7)
1231 or (_is_close_to_int(2 * x) and
1232 int(np.round(2 * x)) == 1)
1233 for x in locs
1234 ):
1235 # minor ticks are subsample from ideal, so no label
1236 return None
1237 if len(locs) < self._minor_threshold:
1238 if len(locs) < self._minor_number:
1239 self._labelled.update(locs)
1240 else:
1241 # we do not have a lot of minor ticks, so only few decades are
1242 # displayed, then we choose some (spaced) minor ticks to label.
1243 # Only minor ticks are known, we assume it is sufficient to
1244 # choice which ticks are displayed.
1245 # For each ticks we compute the distance between the ticks and
1246 # the previous, and between the ticks and the next one. Ticks
1247 # with smallest minimum are chosen. As tiebreak, the ticks
1248 # with smallest sum is chosen.
1249 diff = np.diff(-np.log(1 / self.locs - 1))
1250 space_pessimistic = np.minimum(
1251 np.concatenate(((np.inf,), diff)),
1252 np.concatenate((diff, (np.inf,))),
1253 )
1254 space_sum = (
1255 np.concatenate(((0,), diff))
1256 + np.concatenate((diff, (0,)))
1257 )
1258 good_minor = sorted(
1259 range(len(self.locs)),
1260 key=lambda i: (space_pessimistic[i], space_sum[i]),
1261 )[-self._minor_number:]
1262 self._labelled.update(locs[i] for i in good_minor)
1263
1264 def _format_value(self, x, locs, sci_notation=True):
1265 if sci_notation:
1266 exponent = math.floor(np.log10(x))
1267 min_precision = 0
1268 else:
1269 exponent = 0
1270 min_precision = 1
1271 value = x * 10 ** (-exponent)
1272 if len(locs) < 2:
1273 precision = min_precision
1274 else:
1275 diff = np.sort(np.abs(locs - x))[1]
1276 precision = -np.log10(diff) + exponent
1277 precision = (
1278 int(np.round(precision))
1279 if _is_close_to_int(precision)
1280 else math.ceil(precision)
1281 )
1282 if precision < min_precision:
1283 precision = min_precision
1284 mantissa = r"%.*f" % (precision, value)
1285 if not sci_notation:
1286 return mantissa
1287 s = r"%s\cdot10^{%d}" % (mantissa, exponent)
1288 return s
1289
1290 def _one_minus(self, s):
1291 if self._use_overline:
1292 return r"\overline{%s}" % s
1293 else:
1294 return f"1-{s}"
1295
1296 def __call__(self, x, pos=None):
1297 if self._minor and x not in self._labelled:
1298 return ""
1299 if x <= 0 or x >= 1:
1300 return ""
1301 if _is_close_to_int(2 * x) and round(2 * x) == 1:
1302 s = self._one_half
1303 elif x < 0.5 and _is_decade(x, rtol=1e-7):
1304 exponent = round(math.log10(x))
1305 s = "10^{%d}" % exponent
1306 elif x > 0.5 and _is_decade(1 - x, rtol=1e-7):
1307 exponent = round(math.log10(1 - x))
1308 s = self._one_minus("10^{%d}" % exponent)
1309 elif x < 0.1:
1310 s = self._format_value(x, self.locs)
1311 elif x > 0.9:
1312 s = self._one_minus(self._format_value(1-x, 1-self.locs))
1313 else:
1314 s = self._format_value(x, self.locs, sci_notation=False)
1315 return r"$\mathdefault{%s}$" % s
1316
1317 def format_data_short(self, value):
1318 # docstring inherited
1319 # Thresholds chosen to use scientific notation iff exponent <= -2.
1320 if value < 0.1:
1321 return f"{value:e}"
1322 if value < 0.9:
1323 return f"{value:f}"
1324 return f"1-{1 - value:e}"
1325
1326
1327class EngFormatter(Formatter):
1328 """
1329 Format axis values using engineering prefixes to represent powers
1330 of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7.
1331 """
1332
1333 # The SI engineering prefixes
1334 ENG_PREFIXES = {
1335 -30: "q",
1336 -27: "r",
1337 -24: "y",
1338 -21: "z",
1339 -18: "a",
1340 -15: "f",
1341 -12: "p",
1342 -9: "n",
1343 -6: "\N{MICRO SIGN}",
1344 -3: "m",
1345 0: "",
1346 3: "k",
1347 6: "M",
1348 9: "G",
1349 12: "T",
1350 15: "P",
1351 18: "E",
1352 21: "Z",
1353 24: "Y",
1354 27: "R",
1355 30: "Q"
1356 }
1357
1358 def __init__(self, unit="", places=None, sep=" ", *, usetex=None,
1359 useMathText=None):
1360 r"""
1361 Parameters
1362 ----------
1363 unit : str, default: ""
1364 Unit symbol to use, suitable for use with single-letter
1365 representations of powers of 1000. For example, 'Hz' or 'm'.
1366
1367 places : int, default: None
1368 Precision with which to display the number, specified in
1369 digits after the decimal point (there will be between one
1370 and three digits before the decimal point). If it is None,
1371 the formatting falls back to the floating point format '%g',
1372 which displays up to 6 *significant* digits, i.e. the equivalent
1373 value for *places* varies between 0 and 5 (inclusive).
1374
1375 sep : str, default: " "
1376 Separator used between the value and the prefix/unit. For
1377 example, one get '3.14 mV' if ``sep`` is " " (default) and
1378 '3.14mV' if ``sep`` is "". Besides the default behavior, some
1379 other useful options may be:
1380
1381 * ``sep=""`` to append directly the prefix/unit to the value;
1382 * ``sep="\N{THIN SPACE}"`` (``U+2009``);
1383 * ``sep="\N{NARROW NO-BREAK SPACE}"`` (``U+202F``);
1384 * ``sep="\N{NO-BREAK SPACE}"`` (``U+00A0``).
1385
1386 usetex : bool, default: :rc:`text.usetex`
1387 To enable/disable the use of TeX's math mode for rendering the
1388 numbers in the formatter.
1389
1390 useMathText : bool, default: :rc:`axes.formatter.use_mathtext`
1391 To enable/disable the use mathtext for rendering the numbers in
1392 the formatter.
1393 """
1394 self.unit = unit
1395 self.places = places
1396 self.sep = sep
1397 self.set_usetex(usetex)
1398 self.set_useMathText(useMathText)
1399
1400 def get_usetex(self):
1401 return self._usetex
1402
1403 def set_usetex(self, val):
1404 if val is None:
1405 self._usetex = mpl.rcParams['text.usetex']
1406 else:
1407 self._usetex = val
1408
1409 usetex = property(fget=get_usetex, fset=set_usetex)
1410
1411 def get_useMathText(self):
1412 return self._useMathText
1413
1414 def set_useMathText(self, val):
1415 if val is None:
1416 self._useMathText = mpl.rcParams['axes.formatter.use_mathtext']
1417 else:
1418 self._useMathText = val
1419
1420 useMathText = property(fget=get_useMathText, fset=set_useMathText)
1421
1422 def __call__(self, x, pos=None):
1423 s = f"{self.format_eng(x)}{self.unit}"
1424 # Remove the trailing separator when there is neither prefix nor unit
1425 if self.sep and s.endswith(self.sep):
1426 s = s[:-len(self.sep)]
1427 return self.fix_minus(s)
1428
1429 def format_eng(self, num):
1430 """
1431 Format a number in engineering notation, appending a letter
1432 representing the power of 1000 of the original number.
1433 Some examples:
1434
1435 >>> format_eng(0) # for self.places = 0
1436 '0'
1437
1438 >>> format_eng(1000000) # for self.places = 1
1439 '1.0 M'
1440
1441 >>> format_eng(-1e-6) # for self.places = 2
1442 '-1.00 \N{MICRO SIGN}'
1443 """
1444 sign = 1
1445 fmt = "g" if self.places is None else f".{self.places:d}f"
1446
1447 if num < 0:
1448 sign = -1
1449 num = -num
1450
1451 if num != 0:
1452 pow10 = int(math.floor(math.log10(num) / 3) * 3)
1453 else:
1454 pow10 = 0
1455 # Force num to zero, to avoid inconsistencies like
1456 # format_eng(-0) = "0" and format_eng(0.0) = "0"
1457 # but format_eng(-0.0) = "-0.0"
1458 num = 0.0
1459
1460 pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES))
1461
1462 mant = sign * num / (10.0 ** pow10)
1463 # Taking care of the cases like 999.9..., which may be rounded to 1000
1464 # instead of 1 k. Beware of the corner case of values that are beyond
1465 # the range of SI prefixes (i.e. > 'Y').
1466 if (abs(float(format(mant, fmt))) >= 1000
1467 and pow10 < max(self.ENG_PREFIXES)):
1468 mant /= 1000
1469 pow10 += 3
1470
1471 prefix = self.ENG_PREFIXES[int(pow10)]
1472 if self._usetex or self._useMathText:
1473 formatted = f"${mant:{fmt}}${self.sep}{prefix}"
1474 else:
1475 formatted = f"{mant:{fmt}}{self.sep}{prefix}"
1476
1477 return formatted
1478
1479
1480class PercentFormatter(Formatter):
1481 """
1482 Format numbers as a percentage.
1483
1484 Parameters
1485 ----------
1486 xmax : float
1487 Determines how the number is converted into a percentage.
1488 *xmax* is the data value that corresponds to 100%.
1489 Percentages are computed as ``x / xmax * 100``. So if the data is
1490 already scaled to be percentages, *xmax* will be 100. Another common
1491 situation is where *xmax* is 1.0.
1492
1493 decimals : None or int
1494 The number of decimal places to place after the point.
1495 If *None* (the default), the number will be computed automatically.
1496
1497 symbol : str or None
1498 A string that will be appended to the label. It may be
1499 *None* or empty to indicate that no symbol should be used. LaTeX
1500 special characters are escaped in *symbol* whenever latex mode is
1501 enabled, unless *is_latex* is *True*.
1502
1503 is_latex : bool
1504 If *False*, reserved LaTeX characters in *symbol* will be escaped.
1505 """
1506 def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False):
1507 self.xmax = xmax + 0.0
1508 self.decimals = decimals
1509 self._symbol = symbol
1510 self._is_latex = is_latex
1511
1512 def __call__(self, x, pos=None):
1513 """Format the tick as a percentage with the appropriate scaling."""
1514 ax_min, ax_max = self.axis.get_view_interval()
1515 display_range = abs(ax_max - ax_min)
1516 return self.fix_minus(self.format_pct(x, display_range))
1517
1518 def format_pct(self, x, display_range):
1519 """
1520 Format the number as a percentage number with the correct
1521 number of decimals and adds the percent symbol, if any.
1522
1523 If ``self.decimals`` is `None`, the number of digits after the
1524 decimal point is set based on the *display_range* of the axis
1525 as follows:
1526
1527 ============= ======== =======================
1528 display_range decimals sample
1529 ============= ======== =======================
1530 >50 0 ``x = 34.5`` => 35%
1531 >5 1 ``x = 34.5`` => 34.5%
1532 >0.5 2 ``x = 34.5`` => 34.50%
1533 ... ... ...
1534 ============= ======== =======================
1535
1536 This method will not be very good for tiny axis ranges or
1537 extremely large ones. It assumes that the values on the chart
1538 are percentages displayed on a reasonable scale.
1539 """
1540 x = self.convert_to_pct(x)
1541 if self.decimals is None:
1542 # conversion works because display_range is a difference
1543 scaled_range = self.convert_to_pct(display_range)
1544 if scaled_range <= 0:
1545 decimals = 0
1546 else:
1547 # Luckily Python's built-in ceil rounds to +inf, not away from
1548 # zero. This is very important since the equation for decimals
1549 # starts out as `scaled_range > 0.5 * 10**(2 - decimals)`
1550 # and ends up with `decimals > 2 - log10(2 * scaled_range)`.
1551 decimals = math.ceil(2.0 - math.log10(2.0 * scaled_range))
1552 if decimals > 5:
1553 decimals = 5
1554 elif decimals < 0:
1555 decimals = 0
1556 else:
1557 decimals = self.decimals
1558 s = f'{x:0.{int(decimals)}f}'
1559
1560 return s + self.symbol
1561
1562 def convert_to_pct(self, x):
1563 return 100.0 * (x / self.xmax)
1564
1565 @property
1566 def symbol(self):
1567 r"""
1568 The configured percent symbol as a string.
1569
1570 If LaTeX is enabled via :rc:`text.usetex`, the special characters
1571 ``{'#', '$', '%', '&', '~', '_', '^', '\', '{', '}'}`` are
1572 automatically escaped in the string.
1573 """
1574 symbol = self._symbol
1575 if not symbol:
1576 symbol = ''
1577 elif not self._is_latex and mpl.rcParams['text.usetex']:
1578 # Source: http://www.personal.ceu.hu/tex/specchar.htm
1579 # Backslash must be first for this to work correctly since
1580 # it keeps getting added in
1581 for spec in r'\#$%&~_^{}':
1582 symbol = symbol.replace(spec, '\\' + spec)
1583 return symbol
1584
1585 @symbol.setter
1586 def symbol(self, symbol):
1587 self._symbol = symbol
1588
1589
1590class Locator(TickHelper):
1591 """
1592 Determine tick locations.
1593
1594 Note that the same locator should not be used across multiple
1595 `~matplotlib.axis.Axis` because the locator stores references to the Axis
1596 data and view limits.
1597 """
1598
1599 # Some automatic tick locators can generate so many ticks they
1600 # kill the machine when you try and render them.
1601 # This parameter is set to cause locators to raise an error if too
1602 # many ticks are generated.
1603 MAXTICKS = 1000
1604
1605 def tick_values(self, vmin, vmax):
1606 """
1607 Return the values of the located ticks given **vmin** and **vmax**.
1608
1609 .. note::
1610 To get tick locations with the vmin and vmax values defined
1611 automatically for the associated ``axis`` simply call
1612 the Locator instance::
1613
1614 >>> print(type(loc))
1615 <type 'Locator'>
1616 >>> print(loc())
1617 [1, 2, 3, 4]
1618
1619 """
1620 raise NotImplementedError('Derived must override')
1621
1622 def set_params(self, **kwargs):
1623 """
1624 Do nothing, and raise a warning. Any locator class not supporting the
1625 set_params() function will call this.
1626 """
1627 _api.warn_external(
1628 "'set_params()' not defined for locator of type " +
1629 str(type(self)))
1630
1631 def __call__(self):
1632 """Return the locations of the ticks."""
1633 # note: some locators return data limits, other return view limits,
1634 # hence there is no *one* interface to call self.tick_values.
1635 raise NotImplementedError('Derived must override')
1636
1637 def raise_if_exceeds(self, locs):
1638 """
1639 Log at WARNING level if *locs* is longer than `Locator.MAXTICKS`.
1640
1641 This is intended to be called immediately before returning *locs* from
1642 ``__call__`` to inform users in case their Locator returns a huge
1643 number of ticks, causing Matplotlib to run out of memory.
1644
1645 The "strange" name of this method dates back to when it would raise an
1646 exception instead of emitting a log.
1647 """
1648 if len(locs) >= self.MAXTICKS:
1649 _log.warning(
1650 "Locator attempting to generate %s ticks ([%s, ..., %s]), "
1651 "which exceeds Locator.MAXTICKS (%s).",
1652 len(locs), locs[0], locs[-1], self.MAXTICKS)
1653 return locs
1654
1655 def nonsingular(self, v0, v1):
1656 """
1657 Adjust a range as needed to avoid singularities.
1658
1659 This method gets called during autoscaling, with ``(v0, v1)`` set to
1660 the data limits on the Axes if the Axes contains any data, or
1661 ``(-inf, +inf)`` if not.
1662
1663 - If ``v0 == v1`` (possibly up to some floating point slop), this
1664 method returns an expanded interval around this value.
1665 - If ``(v0, v1) == (-inf, +inf)``, this method returns appropriate
1666 default view limits.
1667 - Otherwise, ``(v0, v1)`` is returned without modification.
1668 """
1669 return mtransforms.nonsingular(v0, v1, expander=.05)
1670
1671 def view_limits(self, vmin, vmax):
1672 """
1673 Select a scale for the range from vmin to vmax.
1674
1675 Subclasses should override this method to change locator behaviour.
1676 """
1677 return mtransforms.nonsingular(vmin, vmax)
1678
1679
1680class IndexLocator(Locator):
1681 """
1682 Place ticks at every nth point plotted.
1683
1684 IndexLocator assumes index plotting; i.e., that the ticks are placed at integer
1685 values in the range between 0 and len(data) inclusive.
1686 """
1687 def __init__(self, base, offset):
1688 """Place ticks every *base* data point, starting at *offset*."""
1689 self._base = base
1690 self.offset = offset
1691
1692 def set_params(self, base=None, offset=None):
1693 """Set parameters within this locator"""
1694 if base is not None:
1695 self._base = base
1696 if offset is not None:
1697 self.offset = offset
1698
1699 def __call__(self):
1700 """Return the locations of the ticks"""
1701 dmin, dmax = self.axis.get_data_interval()
1702 return self.tick_values(dmin, dmax)
1703
1704 def tick_values(self, vmin, vmax):
1705 return self.raise_if_exceeds(
1706 np.arange(vmin + self.offset, vmax + 1, self._base))
1707
1708
1709class FixedLocator(Locator):
1710 r"""
1711 Place ticks at a set of fixed values.
1712
1713 If *nbins* is None ticks are placed at all values. Otherwise, the *locs* array of
1714 possible positions will be subsampled to keep the number of ticks
1715 :math:`\leq nbins + 1`. The subsampling will be done to include the smallest
1716 absolute value; for example, if zero is included in the array of possibilities, then
1717 it will be included in the chosen ticks.
1718 """
1719
1720 def __init__(self, locs, nbins=None):
1721 self.locs = np.asarray(locs)
1722 _api.check_shape((None,), locs=self.locs)
1723 self.nbins = max(nbins, 2) if nbins is not None else None
1724
1725 def set_params(self, nbins=None):
1726 """Set parameters within this locator."""
1727 if nbins is not None:
1728 self.nbins = nbins
1729
1730 def __call__(self):
1731 return self.tick_values(None, None)
1732
1733 def tick_values(self, vmin, vmax):
1734 """
1735 Return the locations of the ticks.
1736
1737 .. note::
1738
1739 Because the values are fixed, vmin and vmax are not used in this
1740 method.
1741
1742 """
1743 if self.nbins is None:
1744 return self.locs
1745 step = max(int(np.ceil(len(self.locs) / self.nbins)), 1)
1746 ticks = self.locs[::step]
1747 for i in range(1, step):
1748 ticks1 = self.locs[i::step]
1749 if np.abs(ticks1).min() < np.abs(ticks).min():
1750 ticks = ticks1
1751 return self.raise_if_exceeds(ticks)
1752
1753
1754class NullLocator(Locator):
1755 """
1756 No ticks
1757 """
1758
1759 def __call__(self):
1760 return self.tick_values(None, None)
1761
1762 def tick_values(self, vmin, vmax):
1763 """
1764 Return the locations of the ticks.
1765
1766 .. note::
1767
1768 Because the values are Null, vmin and vmax are not used in this
1769 method.
1770 """
1771 return []
1772
1773
1774class LinearLocator(Locator):
1775 """
1776 Place ticks at evenly spaced values.
1777
1778 The first time this function is called it will try to set the
1779 number of ticks to make a nice tick partitioning. Thereafter, the
1780 number of ticks will be fixed so that interactive navigation will
1781 be nice
1782
1783 """
1784 def __init__(self, numticks=None, presets=None):
1785 """
1786 Parameters
1787 ----------
1788 numticks : int or None, default None
1789 Number of ticks. If None, *numticks* = 11.
1790 presets : dict or None, default: None
1791 Dictionary mapping ``(vmin, vmax)`` to an array of locations.
1792 Overrides *numticks* if there is an entry for the current
1793 ``(vmin, vmax)``.
1794 """
1795 self.numticks = numticks
1796 if presets is None:
1797 self.presets = {}
1798 else:
1799 self.presets = presets
1800
1801 @property
1802 def numticks(self):
1803 # Old hard-coded default.
1804 return self._numticks if self._numticks is not None else 11
1805
1806 @numticks.setter
1807 def numticks(self, numticks):
1808 self._numticks = numticks
1809
1810 def set_params(self, numticks=None, presets=None):
1811 """Set parameters within this locator."""
1812 if presets is not None:
1813 self.presets = presets
1814 if numticks is not None:
1815 self.numticks = numticks
1816
1817 def __call__(self):
1818 """Return the locations of the ticks."""
1819 vmin, vmax = self.axis.get_view_interval()
1820 return self.tick_values(vmin, vmax)
1821
1822 def tick_values(self, vmin, vmax):
1823 vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05)
1824
1825 if (vmin, vmax) in self.presets:
1826 return self.presets[(vmin, vmax)]
1827
1828 if self.numticks == 0:
1829 return []
1830 ticklocs = np.linspace(vmin, vmax, self.numticks)
1831
1832 return self.raise_if_exceeds(ticklocs)
1833
1834 def view_limits(self, vmin, vmax):
1835 """Try to choose the view limits intelligently."""
1836
1837 if vmax < vmin:
1838 vmin, vmax = vmax, vmin
1839
1840 if vmin == vmax:
1841 vmin -= 1
1842 vmax += 1
1843
1844 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
1845 exponent, remainder = divmod(
1846 math.log10(vmax - vmin), math.log10(max(self.numticks - 1, 1)))
1847 exponent -= (remainder < .5)
1848 scale = max(self.numticks - 1, 1) ** (-exponent)
1849 vmin = math.floor(scale * vmin) / scale
1850 vmax = math.ceil(scale * vmax) / scale
1851
1852 return mtransforms.nonsingular(vmin, vmax)
1853
1854
1855class MultipleLocator(Locator):
1856 """
1857 Place ticks at every integer multiple of a base plus an offset.
1858 """
1859
1860 def __init__(self, base=1.0, offset=0.0):
1861 """
1862 Parameters
1863 ----------
1864 base : float > 0, default: 1.0
1865 Interval between ticks.
1866 offset : float, default: 0.0
1867 Value added to each multiple of *base*.
1868
1869 .. versionadded:: 3.8
1870 """
1871 self._edge = _Edge_integer(base, 0)
1872 self._offset = offset
1873
1874 def set_params(self, base=None, offset=None):
1875 """
1876 Set parameters within this locator.
1877
1878 Parameters
1879 ----------
1880 base : float > 0, optional
1881 Interval between ticks.
1882 offset : float, optional
1883 Value added to each multiple of *base*.
1884
1885 .. versionadded:: 3.8
1886 """
1887 if base is not None:
1888 self._edge = _Edge_integer(base, 0)
1889 if offset is not None:
1890 self._offset = offset
1891
1892 def __call__(self):
1893 """Return the locations of the ticks."""
1894 vmin, vmax = self.axis.get_view_interval()
1895 return self.tick_values(vmin, vmax)
1896
1897 def tick_values(self, vmin, vmax):
1898 if vmax < vmin:
1899 vmin, vmax = vmax, vmin
1900 step = self._edge.step
1901 vmin -= self._offset
1902 vmax -= self._offset
1903 vmin = self._edge.ge(vmin) * step
1904 n = (vmax - vmin + 0.001 * step) // step
1905 locs = vmin - step + np.arange(n + 3) * step + self._offset
1906 return self.raise_if_exceeds(locs)
1907
1908 def view_limits(self, dmin, dmax):
1909 """
1910 Set the view limits to the nearest tick values that contain the data.
1911 """
1912 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
1913 vmin = self._edge.le(dmin - self._offset) * self._edge.step + self._offset
1914 vmax = self._edge.ge(dmax - self._offset) * self._edge.step + self._offset
1915 if vmin == vmax:
1916 vmin -= 1
1917 vmax += 1
1918 else:
1919 vmin = dmin
1920 vmax = dmax
1921
1922 return mtransforms.nonsingular(vmin, vmax)
1923
1924
1925def scale_range(vmin, vmax, n=1, threshold=100):
1926 dv = abs(vmax - vmin) # > 0 as nonsingular is called before.
1927 meanv = (vmax + vmin) / 2
1928 if abs(meanv) / dv < threshold:
1929 offset = 0
1930 else:
1931 offset = math.copysign(10 ** (math.log10(abs(meanv)) // 1), meanv)
1932 scale = 10 ** (math.log10(dv / n) // 1)
1933 return scale, offset
1934
1935
1936class _Edge_integer:
1937 """
1938 Helper for `.MaxNLocator`, `.MultipleLocator`, etc.
1939
1940 Take floating-point precision limitations into account when calculating
1941 tick locations as integer multiples of a step.
1942 """
1943 def __init__(self, step, offset):
1944 """
1945 Parameters
1946 ----------
1947 step : float > 0
1948 Interval between ticks.
1949 offset : float
1950 Offset subtracted from the data limits prior to calculating tick
1951 locations.
1952 """
1953 if step <= 0:
1954 raise ValueError("'step' must be positive")
1955 self.step = step
1956 self._offset = abs(offset)
1957
1958 def closeto(self, ms, edge):
1959 # Allow more slop when the offset is large compared to the step.
1960 if self._offset > 0:
1961 digits = np.log10(self._offset / self.step)
1962 tol = max(1e-10, 10 ** (digits - 12))
1963 tol = min(0.4999, tol)
1964 else:
1965 tol = 1e-10
1966 return abs(ms - edge) < tol
1967
1968 def le(self, x):
1969 """Return the largest n: n*step <= x."""
1970 d, m = divmod(x, self.step)
1971 if self.closeto(m / self.step, 1):
1972 return d + 1
1973 return d
1974
1975 def ge(self, x):
1976 """Return the smallest n: n*step >= x."""
1977 d, m = divmod(x, self.step)
1978 if self.closeto(m / self.step, 0):
1979 return d
1980 return d + 1
1981
1982
1983class MaxNLocator(Locator):
1984 """
1985 Place evenly spaced ticks, with a cap on the total number of ticks.
1986
1987 Finds nice tick locations with no more than :math:`nbins + 1` ticks being within the
1988 view limits. Locations beyond the limits are added to support autoscaling.
1989 """
1990 default_params = dict(nbins=10,
1991 steps=None,
1992 integer=False,
1993 symmetric=False,
1994 prune=None,
1995 min_n_ticks=2)
1996
1997 def __init__(self, nbins=None, **kwargs):
1998 """
1999 Parameters
2000 ----------
2001 nbins : int or 'auto', default: 10
2002 Maximum number of intervals; one less than max number of
2003 ticks. If the string 'auto', the number of bins will be
2004 automatically determined based on the length of the axis.
2005
2006 steps : array-like, optional
2007 Sequence of acceptable tick multiples, starting with 1 and
2008 ending with 10. For example, if ``steps=[1, 2, 4, 5, 10]``,
2009 ``20, 40, 60`` or ``0.4, 0.6, 0.8`` would be possible
2010 sets of ticks because they are multiples of 2.
2011 ``30, 60, 90`` would not be generated because 3 does not
2012 appear in this example list of steps.
2013
2014 integer : bool, default: False
2015 If True, ticks will take only integer values, provided at least
2016 *min_n_ticks* integers are found within the view limits.
2017
2018 symmetric : bool, default: False
2019 If True, autoscaling will result in a range symmetric about zero.
2020
2021 prune : {'lower', 'upper', 'both', None}, default: None
2022 Remove the 'lower' tick, the 'upper' tick, or ticks on 'both' sides
2023 *if they fall exactly on an axis' edge* (this typically occurs when
2024 :rc:`axes.autolimit_mode` is 'round_numbers'). Removing such ticks
2025 is mostly useful for stacked or ganged plots, where the upper tick
2026 of an Axes overlaps with the lower tick of the axes above it.
2027
2028 min_n_ticks : int, default: 2
2029 Relax *nbins* and *integer* constraints if necessary to obtain
2030 this minimum number of ticks.
2031 """
2032 if nbins is not None:
2033 kwargs['nbins'] = nbins
2034 self.set_params(**{**self.default_params, **kwargs})
2035
2036 @staticmethod
2037 def _validate_steps(steps):
2038 if not np.iterable(steps):
2039 raise ValueError('steps argument must be an increasing sequence '
2040 'of numbers between 1 and 10 inclusive')
2041 steps = np.asarray(steps)
2042 if np.any(np.diff(steps) <= 0) or steps[-1] > 10 or steps[0] < 1:
2043 raise ValueError('steps argument must be an increasing sequence '
2044 'of numbers between 1 and 10 inclusive')
2045 if steps[0] != 1:
2046 steps = np.concatenate([[1], steps])
2047 if steps[-1] != 10:
2048 steps = np.concatenate([steps, [10]])
2049 return steps
2050
2051 @staticmethod
2052 def _staircase(steps):
2053 # Make an extended staircase within which the needed step will be
2054 # found. This is probably much larger than necessary.
2055 return np.concatenate([0.1 * steps[:-1], steps, [10 * steps[1]]])
2056
2057 def set_params(self, **kwargs):
2058 """
2059 Set parameters for this locator.
2060
2061 Parameters
2062 ----------
2063 nbins : int or 'auto', optional
2064 see `.MaxNLocator`
2065 steps : array-like, optional
2066 see `.MaxNLocator`
2067 integer : bool, optional
2068 see `.MaxNLocator`
2069 symmetric : bool, optional
2070 see `.MaxNLocator`
2071 prune : {'lower', 'upper', 'both', None}, optional
2072 see `.MaxNLocator`
2073 min_n_ticks : int, optional
2074 see `.MaxNLocator`
2075 """
2076 if 'nbins' in kwargs:
2077 self._nbins = kwargs.pop('nbins')
2078 if self._nbins != 'auto':
2079 self._nbins = int(self._nbins)
2080 if 'symmetric' in kwargs:
2081 self._symmetric = kwargs.pop('symmetric')
2082 if 'prune' in kwargs:
2083 prune = kwargs.pop('prune')
2084 _api.check_in_list(['upper', 'lower', 'both', None], prune=prune)
2085 self._prune = prune
2086 if 'min_n_ticks' in kwargs:
2087 self._min_n_ticks = max(1, kwargs.pop('min_n_ticks'))
2088 if 'steps' in kwargs:
2089 steps = kwargs.pop('steps')
2090 if steps is None:
2091 self._steps = np.array([1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10])
2092 else:
2093 self._steps = self._validate_steps(steps)
2094 self._extended_steps = self._staircase(self._steps)
2095 if 'integer' in kwargs:
2096 self._integer = kwargs.pop('integer')
2097 if kwargs:
2098 raise _api.kwarg_error("set_params", kwargs)
2099
2100 def _raw_ticks(self, vmin, vmax):
2101 """
2102 Generate a list of tick locations including the range *vmin* to
2103 *vmax*. In some applications, one or both of the end locations
2104 will not be needed, in which case they are trimmed off
2105 elsewhere.
2106 """
2107 if self._nbins == 'auto':
2108 if self.axis is not None:
2109 nbins = np.clip(self.axis.get_tick_space(),
2110 max(1, self._min_n_ticks - 1), 9)
2111 else:
2112 nbins = 9
2113 else:
2114 nbins = self._nbins
2115
2116 scale, offset = scale_range(vmin, vmax, nbins)
2117 _vmin = vmin - offset
2118 _vmax = vmax - offset
2119 steps = self._extended_steps * scale
2120 if self._integer:
2121 # For steps > 1, keep only integer values.
2122 igood = (steps < 1) | (np.abs(steps - np.round(steps)) < 0.001)
2123 steps = steps[igood]
2124
2125 raw_step = ((_vmax - _vmin) / nbins)
2126 if hasattr(self.axis, "axes") and self.axis.axes.name == '3d':
2127 # Due to the change in automargin behavior in mpl3.9, we need to
2128 # adjust the raw step to match the mpl3.8 appearance. The zoom
2129 # factor of 2/48, gives us the 23/24 modifier.
2130 raw_step = raw_step * 23/24
2131 large_steps = steps >= raw_step
2132 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2133 # Classic round_numbers mode may require a larger step.
2134 # Get first multiple of steps that are <= _vmin
2135 floored_vmins = (_vmin // steps) * steps
2136 floored_vmaxs = floored_vmins + steps * nbins
2137 large_steps = large_steps & (floored_vmaxs >= _vmax)
2138
2139 # Find index of smallest large step
2140 if any(large_steps):
2141 istep = np.nonzero(large_steps)[0][0]
2142 else:
2143 istep = len(steps) - 1
2144
2145 # Start at smallest of the steps greater than the raw step, and check
2146 # if it provides enough ticks. If not, work backwards through
2147 # smaller steps until one is found that provides enough ticks.
2148 for step in steps[:istep+1][::-1]:
2149
2150 if (self._integer and
2151 np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1):
2152 step = max(1, step)
2153 best_vmin = (_vmin // step) * step
2154
2155 # Find tick locations spanning the vmin-vmax range, taking into
2156 # account degradation of precision when there is a large offset.
2157 # The edge ticks beyond vmin and/or vmax are needed for the
2158 # "round_numbers" autolimit mode.
2159 edge = _Edge_integer(step, offset)
2160 low = edge.le(_vmin - best_vmin)
2161 high = edge.ge(_vmax - best_vmin)
2162 ticks = np.arange(low, high + 1) * step + best_vmin
2163 # Count only the ticks that will be displayed.
2164 nticks = ((ticks <= _vmax) & (ticks >= _vmin)).sum()
2165 if nticks >= self._min_n_ticks:
2166 break
2167 return ticks + offset
2168
2169 def __call__(self):
2170 vmin, vmax = self.axis.get_view_interval()
2171 return self.tick_values(vmin, vmax)
2172
2173 def tick_values(self, vmin, vmax):
2174 if self._symmetric:
2175 vmax = max(abs(vmin), abs(vmax))
2176 vmin = -vmax
2177 vmin, vmax = mtransforms.nonsingular(
2178 vmin, vmax, expander=1e-13, tiny=1e-14)
2179 locs = self._raw_ticks(vmin, vmax)
2180
2181 prune = self._prune
2182 if prune == 'lower':
2183 locs = locs[1:]
2184 elif prune == 'upper':
2185 locs = locs[:-1]
2186 elif prune == 'both':
2187 locs = locs[1:-1]
2188 return self.raise_if_exceeds(locs)
2189
2190 def view_limits(self, dmin, dmax):
2191 if self._symmetric:
2192 dmax = max(abs(dmin), abs(dmax))
2193 dmin = -dmax
2194
2195 dmin, dmax = mtransforms.nonsingular(
2196 dmin, dmax, expander=1e-12, tiny=1e-13)
2197
2198 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2199 return self._raw_ticks(dmin, dmax)[[0, -1]]
2200 else:
2201 return dmin, dmax
2202
2203
2204def _is_decade(x, *, base=10, rtol=None):
2205 """Return True if *x* is an integer power of *base*."""
2206 if not np.isfinite(x):
2207 return False
2208 if x == 0.0:
2209 return True
2210 lx = np.log(abs(x)) / np.log(base)
2211 if rtol is None:
2212 return np.isclose(lx, np.round(lx))
2213 else:
2214 return np.isclose(lx, np.round(lx), rtol=rtol)
2215
2216
2217def _decade_less_equal(x, base):
2218 """
2219 Return the largest integer power of *base* that's less or equal to *x*.
2220
2221 If *x* is negative, the exponent will be *greater*.
2222 """
2223 return (x if x == 0 else
2224 -_decade_greater_equal(-x, base) if x < 0 else
2225 base ** np.floor(np.log(x) / np.log(base)))
2226
2227
2228def _decade_greater_equal(x, base):
2229 """
2230 Return the smallest integer power of *base* that's greater or equal to *x*.
2231
2232 If *x* is negative, the exponent will be *smaller*.
2233 """
2234 return (x if x == 0 else
2235 -_decade_less_equal(-x, base) if x < 0 else
2236 base ** np.ceil(np.log(x) / np.log(base)))
2237
2238
2239def _decade_less(x, base):
2240 """
2241 Return the largest integer power of *base* that's less than *x*.
2242
2243 If *x* is negative, the exponent will be *greater*.
2244 """
2245 if x < 0:
2246 return -_decade_greater(-x, base)
2247 less = _decade_less_equal(x, base)
2248 if less == x:
2249 less /= base
2250 return less
2251
2252
2253def _decade_greater(x, base):
2254 """
2255 Return the smallest integer power of *base* that's greater than *x*.
2256
2257 If *x* is negative, the exponent will be *smaller*.
2258 """
2259 if x < 0:
2260 return -_decade_less(-x, base)
2261 greater = _decade_greater_equal(x, base)
2262 if greater == x:
2263 greater *= base
2264 return greater
2265
2266
2267def _is_close_to_int(x):
2268 return math.isclose(x, round(x))
2269
2270
2271class LogLocator(Locator):
2272 """
2273 Place logarithmically spaced ticks.
2274
2275 Places ticks at the values ``subs[j] * base**i``.
2276 """
2277
2278 @_api.delete_parameter("3.8", "numdecs")
2279 def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None):
2280 """
2281 Parameters
2282 ----------
2283 base : float, default: 10.0
2284 The base of the log used, so major ticks are placed at ``base**n``, where
2285 ``n`` is an integer.
2286 subs : None or {'auto', 'all'} or sequence of float, default: (1.0,)
2287 Gives the multiples of integer powers of the base at which to place ticks.
2288 The default of ``(1.0, )`` places ticks only at integer powers of the base.
2289 Permitted string values are ``'auto'`` and ``'all'``. Both of these use an
2290 algorithm based on the axis view limits to determine whether and how to put
2291 ticks between integer powers of the base:
2292 - ``'auto'``: Ticks are placed only between integer powers.
2293 - ``'all'``: Ticks are placed between *and* at integer powers.
2294 - ``None``: Equivalent to ``'auto'``.
2295 numticks : None or int, default: None
2296 The maximum number of ticks to allow on a given axis. The default of
2297 ``None`` will try to choose intelligently as long as this Locator has
2298 already been assigned to an axis using `~.axis.Axis.get_tick_space`, but
2299 otherwise falls back to 9.
2300 """
2301 if numticks is None:
2302 if mpl.rcParams['_internal.classic_mode']:
2303 numticks = 15
2304 else:
2305 numticks = 'auto'
2306 self._base = float(base)
2307 self._set_subs(subs)
2308 self._numdecs = numdecs
2309 self.numticks = numticks
2310
2311 @_api.delete_parameter("3.8", "numdecs")
2312 def set_params(self, base=None, subs=None, numdecs=None, numticks=None):
2313 """Set parameters within this locator."""
2314 if base is not None:
2315 self._base = float(base)
2316 if subs is not None:
2317 self._set_subs(subs)
2318 if numdecs is not None:
2319 self._numdecs = numdecs
2320 if numticks is not None:
2321 self.numticks = numticks
2322
2323 numdecs = _api.deprecate_privatize_attribute(
2324 "3.8", addendum="This attribute has no effect.")
2325
2326 def _set_subs(self, subs):
2327 """
2328 Set the minor ticks for the log scaling every ``base**i*subs[j]``.
2329 """
2330 if subs is None: # consistency with previous bad API
2331 self._subs = 'auto'
2332 elif isinstance(subs, str):
2333 _api.check_in_list(('all', 'auto'), subs=subs)
2334 self._subs = subs
2335 else:
2336 try:
2337 self._subs = np.asarray(subs, dtype=float)
2338 except ValueError as e:
2339 raise ValueError("subs must be None, 'all', 'auto' or "
2340 "a sequence of floats, not "
2341 f"{subs}.") from e
2342 if self._subs.ndim != 1:
2343 raise ValueError("A sequence passed to subs must be "
2344 "1-dimensional, not "
2345 f"{self._subs.ndim}-dimensional.")
2346
2347 def __call__(self):
2348 """Return the locations of the ticks."""
2349 vmin, vmax = self.axis.get_view_interval()
2350 return self.tick_values(vmin, vmax)
2351
2352 def tick_values(self, vmin, vmax):
2353 if self.numticks == 'auto':
2354 if self.axis is not None:
2355 numticks = np.clip(self.axis.get_tick_space(), 2, 9)
2356 else:
2357 numticks = 9
2358 else:
2359 numticks = self.numticks
2360
2361 b = self._base
2362 if vmin <= 0.0:
2363 if self.axis is not None:
2364 vmin = self.axis.get_minpos()
2365
2366 if vmin <= 0.0 or not np.isfinite(vmin):
2367 raise ValueError(
2368 "Data has no positive values, and therefore cannot be log-scaled.")
2369
2370 _log.debug('vmin %s vmax %s', vmin, vmax)
2371
2372 if vmax < vmin:
2373 vmin, vmax = vmax, vmin
2374 log_vmin = math.log(vmin) / math.log(b)
2375 log_vmax = math.log(vmax) / math.log(b)
2376
2377 numdec = math.floor(log_vmax) - math.ceil(log_vmin)
2378
2379 if isinstance(self._subs, str):
2380 if numdec > 10 or b < 3:
2381 if self._subs == 'auto':
2382 return np.array([]) # no minor or major ticks
2383 else:
2384 subs = np.array([1.0]) # major ticks
2385 else:
2386 _first = 2.0 if self._subs == 'auto' else 1.0
2387 subs = np.arange(_first, b)
2388 else:
2389 subs = self._subs
2390
2391 # Get decades between major ticks.
2392 stride = (max(math.ceil(numdec / (numticks - 1)), 1)
2393 if mpl.rcParams['_internal.classic_mode'] else
2394 numdec // numticks + 1)
2395
2396 # if we have decided that the stride is as big or bigger than
2397 # the range, clip the stride back to the available range - 1
2398 # with a floor of 1. This prevents getting axis with only 1 tick
2399 # visible.
2400 if stride >= numdec:
2401 stride = max(1, numdec - 1)
2402
2403 # Does subs include anything other than 1? Essentially a hack to know
2404 # whether we're a major or a minor locator.
2405 have_subs = len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0)
2406
2407 decades = np.arange(math.floor(log_vmin) - stride,
2408 math.ceil(log_vmax) + 2 * stride, stride)
2409
2410 if have_subs:
2411 if stride == 1:
2412 ticklocs = np.concatenate(
2413 [subs * decade_start for decade_start in b ** decades])
2414 else:
2415 ticklocs = np.array([])
2416 else:
2417 ticklocs = b ** decades
2418
2419 _log.debug('ticklocs %r', ticklocs)
2420 if (len(subs) > 1
2421 and stride == 1
2422 and ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() <= 1):
2423 # If we're a minor locator *that expects at least two ticks per
2424 # decade* and the major locator stride is 1 and there's no more
2425 # than one minor tick, switch to AutoLocator.
2426 return AutoLocator().tick_values(vmin, vmax)
2427 else:
2428 return self.raise_if_exceeds(ticklocs)
2429
2430 def view_limits(self, vmin, vmax):
2431 """Try to choose the view limits intelligently."""
2432 b = self._base
2433
2434 vmin, vmax = self.nonsingular(vmin, vmax)
2435
2436 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2437 vmin = _decade_less_equal(vmin, b)
2438 vmax = _decade_greater_equal(vmax, b)
2439
2440 return vmin, vmax
2441
2442 def nonsingular(self, vmin, vmax):
2443 if vmin > vmax:
2444 vmin, vmax = vmax, vmin
2445 if not np.isfinite(vmin) or not np.isfinite(vmax):
2446 vmin, vmax = 1, 10 # Initial range, no data plotted yet.
2447 elif vmax <= 0:
2448 _api.warn_external(
2449 "Data has no positive values, and therefore cannot be "
2450 "log-scaled.")
2451 vmin, vmax = 1, 10
2452 else:
2453 # Consider shared axises
2454 minpos = min(axis.get_minpos() for axis in self.axis._get_shared_axis())
2455 if not np.isfinite(minpos):
2456 minpos = 1e-300 # This should never take effect.
2457 if vmin <= 0:
2458 vmin = minpos
2459 if vmin == vmax:
2460 vmin = _decade_less(vmin, self._base)
2461 vmax = _decade_greater(vmax, self._base)
2462 return vmin, vmax
2463
2464
2465class SymmetricalLogLocator(Locator):
2466 """
2467 Place ticks spaced linearly near zero and spaced logarithmically beyond a threshold.
2468 """
2469
2470 def __init__(self, transform=None, subs=None, linthresh=None, base=None):
2471 """
2472 Parameters
2473 ----------
2474 transform : `~.scale.SymmetricalLogTransform`, optional
2475 If set, defines the *base* and *linthresh* of the symlog transform.
2476 base, linthresh : float, optional
2477 The *base* and *linthresh* of the symlog transform, as documented
2478 for `.SymmetricalLogScale`. These parameters are only used if
2479 *transform* is not set.
2480 subs : sequence of float, default: [1]
2481 The multiples of integer powers of the base where ticks are placed,
2482 i.e., ticks are placed at
2483 ``[sub * base**i for i in ... for sub in subs]``.
2484
2485 Notes
2486 -----
2487 Either *transform*, or both *base* and *linthresh*, must be given.
2488 """
2489 if transform is not None:
2490 self._base = transform.base
2491 self._linthresh = transform.linthresh
2492 elif linthresh is not None and base is not None:
2493 self._base = base
2494 self._linthresh = linthresh
2495 else:
2496 raise ValueError("Either transform, or both linthresh "
2497 "and base, must be provided.")
2498 if subs is None:
2499 self._subs = [1.0]
2500 else:
2501 self._subs = subs
2502 self.numticks = 15
2503
2504 def set_params(self, subs=None, numticks=None):
2505 """Set parameters within this locator."""
2506 if numticks is not None:
2507 self.numticks = numticks
2508 if subs is not None:
2509 self._subs = subs
2510
2511 def __call__(self):
2512 """Return the locations of the ticks."""
2513 # Note, these are untransformed coordinates
2514 vmin, vmax = self.axis.get_view_interval()
2515 return self.tick_values(vmin, vmax)
2516
2517 def tick_values(self, vmin, vmax):
2518 linthresh = self._linthresh
2519
2520 if vmax < vmin:
2521 vmin, vmax = vmax, vmin
2522
2523 # The domain is divided into three sections, only some of
2524 # which may actually be present.
2525 #
2526 # <======== -t ==0== t ========>
2527 # aaaaaaaaa bbbbb ccccccccc
2528 #
2529 # a) and c) will have ticks at integral log positions. The
2530 # number of ticks needs to be reduced if there are more
2531 # than self.numticks of them.
2532 #
2533 # b) has a tick at 0 and only 0 (we assume t is a small
2534 # number, and the linear segment is just an implementation
2535 # detail and not interesting.)
2536 #
2537 # We could also add ticks at t, but that seems to usually be
2538 # uninteresting.
2539 #
2540 # "simple" mode is when the range falls entirely within [-t, t]
2541 # -- it should just display (vmin, 0, vmax)
2542 if -linthresh <= vmin < vmax <= linthresh:
2543 # only the linear range is present
2544 return sorted({vmin, 0, vmax})
2545
2546 # Lower log range is present
2547 has_a = (vmin < -linthresh)
2548 # Upper log range is present
2549 has_c = (vmax > linthresh)
2550
2551 # Check if linear range is present
2552 has_b = (has_a and vmax > -linthresh) or (has_c and vmin < linthresh)
2553
2554 base = self._base
2555
2556 def get_log_range(lo, hi):
2557 lo = np.floor(np.log(lo) / np.log(base))
2558 hi = np.ceil(np.log(hi) / np.log(base))
2559 return lo, hi
2560
2561 # Calculate all the ranges, so we can determine striding
2562 a_lo, a_hi = (0, 0)
2563 if has_a:
2564 a_upper_lim = min(-linthresh, vmax)
2565 a_lo, a_hi = get_log_range(abs(a_upper_lim), abs(vmin) + 1)
2566
2567 c_lo, c_hi = (0, 0)
2568 if has_c:
2569 c_lower_lim = max(linthresh, vmin)
2570 c_lo, c_hi = get_log_range(c_lower_lim, vmax + 1)
2571
2572 # Calculate the total number of integer exponents in a and c ranges
2573 total_ticks = (a_hi - a_lo) + (c_hi - c_lo)
2574 if has_b:
2575 total_ticks += 1
2576 stride = max(total_ticks // (self.numticks - 1), 1)
2577
2578 decades = []
2579 if has_a:
2580 decades.extend(-1 * (base ** (np.arange(a_lo, a_hi,
2581 stride)[::-1])))
2582
2583 if has_b:
2584 decades.append(0.0)
2585
2586 if has_c:
2587 decades.extend(base ** (np.arange(c_lo, c_hi, stride)))
2588
2589 subs = np.asarray(self._subs)
2590
2591 if len(subs) > 1 or subs[0] != 1.0:
2592 ticklocs = []
2593 for decade in decades:
2594 if decade == 0:
2595 ticklocs.append(decade)
2596 else:
2597 ticklocs.extend(subs * decade)
2598 else:
2599 ticklocs = decades
2600
2601 return self.raise_if_exceeds(np.array(ticklocs))
2602
2603 def view_limits(self, vmin, vmax):
2604 """Try to choose the view limits intelligently."""
2605 b = self._base
2606 if vmax < vmin:
2607 vmin, vmax = vmax, vmin
2608
2609 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers':
2610 vmin = _decade_less_equal(vmin, b)
2611 vmax = _decade_greater_equal(vmax, b)
2612 if vmin == vmax:
2613 vmin = _decade_less(vmin, b)
2614 vmax = _decade_greater(vmax, b)
2615
2616 return mtransforms.nonsingular(vmin, vmax)
2617
2618
2619class AsinhLocator(Locator):
2620 """
2621 Place ticks spaced evenly on an inverse-sinh scale.
2622
2623 Generally used with the `~.scale.AsinhScale` class.
2624
2625 .. note::
2626
2627 This API is provisional and may be revised in the future
2628 based on early user feedback.
2629 """
2630 def __init__(self, linear_width, numticks=11, symthresh=0.2,
2631 base=10, subs=None):
2632 """
2633 Parameters
2634 ----------
2635 linear_width : float
2636 The scale parameter defining the extent
2637 of the quasi-linear region.
2638 numticks : int, default: 11
2639 The approximate number of major ticks that will fit
2640 along the entire axis
2641 symthresh : float, default: 0.2
2642 The fractional threshold beneath which data which covers
2643 a range that is approximately symmetric about zero
2644 will have ticks that are exactly symmetric.
2645 base : int, default: 10
2646 The number base used for rounding tick locations
2647 on a logarithmic scale. If this is less than one,
2648 then rounding is to the nearest integer multiple
2649 of powers of ten.
2650 subs : tuple, default: None
2651 Multiples of the number base, typically used
2652 for the minor ticks, e.g. (2, 5) when base=10.
2653 """
2654 super().__init__()
2655 self.linear_width = linear_width
2656 self.numticks = numticks
2657 self.symthresh = symthresh
2658 self.base = base
2659 self.subs = subs
2660
2661 def set_params(self, numticks=None, symthresh=None,
2662 base=None, subs=None):
2663 """Set parameters within this locator."""
2664 if numticks is not None:
2665 self.numticks = numticks
2666 if symthresh is not None:
2667 self.symthresh = symthresh
2668 if base is not None:
2669 self.base = base
2670 if subs is not None:
2671 self.subs = subs if len(subs) > 0 else None
2672
2673 def __call__(self):
2674 vmin, vmax = self.axis.get_view_interval()
2675 if (vmin * vmax) < 0 and abs(1 + vmax / vmin) < self.symthresh:
2676 # Data-range appears to be almost symmetric, so round up:
2677 bound = max(abs(vmin), abs(vmax))
2678 return self.tick_values(-bound, bound)
2679 else:
2680 return self.tick_values(vmin, vmax)
2681
2682 def tick_values(self, vmin, vmax):
2683 # Construct a set of uniformly-spaced "on-screen" locations.
2684 ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax])
2685 / self.linear_width)
2686 ys = np.linspace(ymin, ymax, self.numticks)
2687 zero_dev = abs(ys / (ymax - ymin))
2688 if ymin * ymax < 0:
2689 # Ensure that the zero tick-mark is included, if the axis straddles zero.
2690 ys = np.hstack([ys[(zero_dev > 0.5 / self.numticks)], 0.0])
2691
2692 # Transform the "on-screen" grid to the data space:
2693 xs = self.linear_width * np.sinh(ys / self.linear_width)
2694 zero_xs = (ys == 0)
2695
2696 # Round the data-space values to be intuitive base-n numbers, keeping track of
2697 # positive and negative values separately and carefully treating the zero value.
2698 with np.errstate(divide="ignore"): # base ** log(0) = base ** -inf = 0.
2699 if self.base > 1:
2700 pows = (np.sign(xs)
2701 * self.base ** np.floor(np.log(abs(xs)) / math.log(self.base)))
2702 qs = np.outer(pows, self.subs).flatten() if self.subs else pows
2703 else: # No need to adjust sign(pows), as it cancels out when computing qs.
2704 pows = np.where(zero_xs, 1, 10**np.floor(np.log10(abs(xs))))
2705 qs = pows * np.round(xs / pows)
2706 ticks = np.array(sorted(set(qs)))
2707
2708 return ticks if len(ticks) >= 2 else np.linspace(vmin, vmax, self.numticks)
2709
2710
2711class LogitLocator(MaxNLocator):
2712 """
2713 Place ticks spaced evenly on a logit scale.
2714 """
2715
2716 def __init__(self, minor=False, *, nbins="auto"):
2717 """
2718 Parameters
2719 ----------
2720 nbins : int or 'auto', optional
2721 Number of ticks. Only used if minor is False.
2722 minor : bool, default: False
2723 Indicate if this locator is for minor ticks or not.
2724 """
2725
2726 self._minor = minor
2727 super().__init__(nbins=nbins, steps=[1, 2, 5, 10])
2728
2729 def set_params(self, minor=None, **kwargs):
2730 """Set parameters within this locator."""
2731 if minor is not None:
2732 self._minor = minor
2733 super().set_params(**kwargs)
2734
2735 @property
2736 def minor(self):
2737 return self._minor
2738
2739 @minor.setter
2740 def minor(self, value):
2741 self.set_params(minor=value)
2742
2743 def tick_values(self, vmin, vmax):
2744 # dummy axis has no axes attribute
2745 if hasattr(self.axis, "axes") and self.axis.axes.name == "polar":
2746 raise NotImplementedError("Polar axis cannot be logit scaled yet")
2747
2748 if self._nbins == "auto":
2749 if self.axis is not None:
2750 nbins = self.axis.get_tick_space()
2751 if nbins < 2:
2752 nbins = 2
2753 else:
2754 nbins = 9
2755 else:
2756 nbins = self._nbins
2757
2758 # We define ideal ticks with their index:
2759 # linscale: ... 1e-3 1e-2 1e-1 1/2 1-1e-1 1-1e-2 1-1e-3 ...
2760 # b-scale : ... -3 -2 -1 0 1 2 3 ...
2761 def ideal_ticks(x):
2762 return 10 ** x if x < 0 else 1 - (10 ** (-x)) if x > 0 else 0.5
2763
2764 vmin, vmax = self.nonsingular(vmin, vmax)
2765 binf = int(
2766 np.floor(np.log10(vmin))
2767 if vmin < 0.5
2768 else 0
2769 if vmin < 0.9
2770 else -np.ceil(np.log10(1 - vmin))
2771 )
2772 bsup = int(
2773 np.ceil(np.log10(vmax))
2774 if vmax <= 0.5
2775 else 1
2776 if vmax <= 0.9
2777 else -np.floor(np.log10(1 - vmax))
2778 )
2779 numideal = bsup - binf - 1
2780 if numideal >= 2:
2781 # have 2 or more wanted ideal ticks, so use them as major ticks
2782 if numideal > nbins:
2783 # to many ideal ticks, subsampling ideals for major ticks, and
2784 # take others for minor ticks
2785 subsampling_factor = math.ceil(numideal / nbins)
2786 if self._minor:
2787 ticklocs = [
2788 ideal_ticks(b)
2789 for b in range(binf, bsup + 1)
2790 if (b % subsampling_factor) != 0
2791 ]
2792 else:
2793 ticklocs = [
2794 ideal_ticks(b)
2795 for b in range(binf, bsup + 1)
2796 if (b % subsampling_factor) == 0
2797 ]
2798 return self.raise_if_exceeds(np.array(ticklocs))
2799 if self._minor:
2800 ticklocs = []
2801 for b in range(binf, bsup):
2802 if b < -1:
2803 ticklocs.extend(np.arange(2, 10) * 10 ** b)
2804 elif b == -1:
2805 ticklocs.extend(np.arange(2, 5) / 10)
2806 elif b == 0:
2807 ticklocs.extend(np.arange(6, 9) / 10)
2808 else:
2809 ticklocs.extend(
2810 1 - np.arange(2, 10)[::-1] * 10 ** (-b - 1)
2811 )
2812 return self.raise_if_exceeds(np.array(ticklocs))
2813 ticklocs = [ideal_ticks(b) for b in range(binf, bsup + 1)]
2814 return self.raise_if_exceeds(np.array(ticklocs))
2815 # the scale is zoomed so same ticks as linear scale can be used
2816 if self._minor:
2817 return []
2818 return super().tick_values(vmin, vmax)
2819
2820 def nonsingular(self, vmin, vmax):
2821 standard_minpos = 1e-7
2822 initial_range = (standard_minpos, 1 - standard_minpos)
2823 if vmin > vmax:
2824 vmin, vmax = vmax, vmin
2825 if not np.isfinite(vmin) or not np.isfinite(vmax):
2826 vmin, vmax = initial_range # Initial range, no data plotted yet.
2827 elif vmax <= 0 or vmin >= 1:
2828 # vmax <= 0 occurs when all values are negative
2829 # vmin >= 1 occurs when all values are greater than one
2830 _api.warn_external(
2831 "Data has no values between 0 and 1, and therefore cannot be "
2832 "logit-scaled."
2833 )
2834 vmin, vmax = initial_range
2835 else:
2836 minpos = (
2837 self.axis.get_minpos()
2838 if self.axis is not None
2839 else standard_minpos
2840 )
2841 if not np.isfinite(minpos):
2842 minpos = standard_minpos # This should never take effect.
2843 if vmin <= 0:
2844 vmin = minpos
2845 # NOTE: for vmax, we should query a property similar to get_minpos,
2846 # but related to the maximal, less-than-one data point.
2847 # Unfortunately, Bbox._minpos is defined very deep in the BBox and
2848 # updated with data, so for now we use 1 - minpos as a substitute.
2849 if vmax >= 1:
2850 vmax = 1 - minpos
2851 if vmin == vmax:
2852 vmin, vmax = 0.1 * vmin, 1 - 0.1 * vmin
2853
2854 return vmin, vmax
2855
2856
2857class AutoLocator(MaxNLocator):
2858 """
2859 Place evenly spaced ticks, with the step size and maximum number of ticks chosen
2860 automatically.
2861
2862 This is a subclass of `~matplotlib.ticker.MaxNLocator`, with parameters
2863 *nbins = 'auto'* and *steps = [1, 2, 2.5, 5, 10]*.
2864 """
2865 def __init__(self):
2866 """
2867 To know the values of the non-public parameters, please have a
2868 look to the defaults of `~matplotlib.ticker.MaxNLocator`.
2869 """
2870 if mpl.rcParams['_internal.classic_mode']:
2871 nbins = 9
2872 steps = [1, 2, 5, 10]
2873 else:
2874 nbins = 'auto'
2875 steps = [1, 2, 2.5, 5, 10]
2876 super().__init__(nbins=nbins, steps=steps)
2877
2878
2879class AutoMinorLocator(Locator):
2880 """
2881 Place evenly spaced minor ticks, with the step size and maximum number of ticks
2882 chosen automatically.
2883
2884 The Axis scale must be linear with evenly spaced major ticks .
2885 """
2886
2887 def __init__(self, n=None):
2888 """
2889 *n* is the number of subdivisions of the interval between
2890 major ticks; e.g., n=2 will place a single minor tick midway
2891 between major ticks.
2892
2893 If *n* is omitted or None, the value stored in rcParams will be used.
2894 In case *n* is set to 'auto', it will be set to 4 or 5. If the distance
2895 between the major ticks equals 1, 2.5, 5 or 10 it can be perfectly
2896 divided in 5 equidistant sub-intervals with a length multiple of
2897 0.05. Otherwise it is divided in 4 sub-intervals.
2898 """
2899 self.ndivs = n
2900
2901 def __call__(self):
2902 # docstring inherited
2903 if self.axis.get_scale() == 'log':
2904 _api.warn_external('AutoMinorLocator does not work on logarithmic scales')
2905 return []
2906
2907 majorlocs = np.unique(self.axis.get_majorticklocs())
2908 if len(majorlocs) < 2:
2909 # Need at least two major ticks to find minor tick locations.
2910 # TODO: Figure out a way to still be able to display minor ticks with less
2911 # than two major ticks visible. For now, just display no ticks at all.
2912 return []
2913 majorstep = majorlocs[1] - majorlocs[0]
2914
2915 if self.ndivs is None:
2916 self.ndivs = mpl.rcParams[
2917 'ytick.minor.ndivs' if self.axis.axis_name == 'y'
2918 else 'xtick.minor.ndivs'] # for x and z axis
2919
2920 if self.ndivs == 'auto':
2921 majorstep_mantissa = 10 ** (np.log10(majorstep) % 1)
2922 ndivs = 5 if np.isclose(majorstep_mantissa, [1, 2.5, 5, 10]).any() else 4
2923 else:
2924 ndivs = self.ndivs
2925
2926 minorstep = majorstep / ndivs
2927
2928 vmin, vmax = sorted(self.axis.get_view_interval())
2929 t0 = majorlocs[0]
2930 tmin = round((vmin - t0) / minorstep)
2931 tmax = round((vmax - t0) / minorstep) + 1
2932 locs = (np.arange(tmin, tmax) * minorstep) + t0
2933
2934 return self.raise_if_exceeds(locs)
2935
2936 def tick_values(self, vmin, vmax):
2937 raise NotImplementedError(
2938 f"Cannot get tick locations for a {type(self).__name__}")