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