Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/ticker.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1197 statements  

1""" 

2Tick locating and formatting 

3============================ 

4 

5This module contains classes for configuring tick locating and formatting. 

6Generic tick locators and formatters are provided, as well as domain specific 

7custom ones. 

8 

9Although the locators know nothing about major or minor ticks, they are used 

10by the Axis class to support major and minor tick locating and formatting. 

11 

12.. _tick_locating: 

13.. _locators: 

14 

15Tick locating 

16------------- 

17 

18The Locator class is the base class for all tick locators. The locators 

19handle autoscaling of the view limits based on the data limits, and the 

20choosing of tick locations. A useful semi-automatic tick locator is 

21`MultipleLocator`. It is initialized with a base, e.g., 10, and it picks 

22axis limits and ticks that are multiples of that base. 

23 

24The Locator subclasses defined here are: 

25 

26======================= ======================================================= 

27`AutoLocator` `MaxNLocator` with simple defaults. This is the default 

28 tick locator for most plotting. 

29`MaxNLocator` Finds up to a max number of intervals with ticks at 

30 nice locations. 

31`LinearLocator` Space ticks evenly from min to max. 

32`LogLocator` Space ticks logarithmically from min to max. 

33`MultipleLocator` Ticks and range are a multiple of base; either integer 

34 or float. 

35`FixedLocator` Tick locations are fixed. 

36`IndexLocator` Locator for index plots (e.g., where 

37 ``x = range(len(y))``). 

38`NullLocator` No ticks. 

39`SymmetricalLogLocator` Locator for use with the symlog norm; works like 

40 `LogLocator` for the part outside of the threshold and 

41 adds 0 if inside the limits. 

42`AsinhLocator` Locator for use with the asinh norm, attempting to 

43 space ticks approximately uniformly. 

44`LogitLocator` Locator for logit scaling. 

45`AutoMinorLocator` Locator for minor ticks when the axis is linear and the 

46 major ticks are uniformly spaced. Subdivides the major 

47 tick interval into a specified number of minor 

48 intervals, defaulting to 4 or 5 depending on the major 

49 interval. 

50======================= ======================================================= 

51 

52There are a number of locators specialized for date locations - see 

53the :mod:`.dates` module. 

54 

55You can define your own locator by deriving from Locator. You must 

56override the ``__call__`` method, which returns a sequence of locations, 

57and you will probably want to override the autoscale method to set the 

58view limits from the data limits. 

59 

60If you want to override the default locator, use one of the above or a custom 

61locator and pass it to the x- or y-axis instance. The relevant methods are:: 

62 

63 ax.xaxis.set_major_locator(xmajor_locator) 

64 ax.xaxis.set_minor_locator(xminor_locator) 

65 ax.yaxis.set_major_locator(ymajor_locator) 

66 ax.yaxis.set_minor_locator(yminor_locator) 

67 

68The default minor locator is `NullLocator`, i.e., no minor ticks on by default. 

69 

70.. note:: 

71 `Locator` instances should not be used with more than one 

72 `~matplotlib.axis.Axis` or `~matplotlib.axes.Axes`. So instead of:: 

73 

74 locator = MultipleLocator(5) 

75 ax.xaxis.set_major_locator(locator) 

76 ax2.xaxis.set_major_locator(locator) 

77 

78 do the following instead:: 

79 

80 ax.xaxis.set_major_locator(MultipleLocator(5)) 

81 ax2.xaxis.set_major_locator(MultipleLocator(5)) 

82 

83.. _formatters: 

84 

85Tick formatting 

86--------------- 

87 

88Tick formatting is controlled by classes derived from Formatter. The formatter 

89operates on a single tick value and returns a string to the axis. 

90 

91========================= ===================================================== 

92`NullFormatter` No labels on the ticks. 

93`FixedFormatter` Set the strings manually for the labels. 

94`FuncFormatter` User defined function sets the labels. 

95`StrMethodFormatter` Use string `format` method. 

96`FormatStrFormatter` Use an old-style sprintf format string. 

97`ScalarFormatter` Default formatter for scalars: autopick the format 

98 string. 

99`LogFormatter` Formatter for log axes. 

100`LogFormatterExponent` Format values for log axis using 

101 ``exponent = log_base(value)``. 

102`LogFormatterMathtext` Format values for log axis using 

103 ``exponent = log_base(value)`` using Math text. 

104`LogFormatterSciNotation` Format values for log axis using scientific notation. 

105`LogitFormatter` Probability formatter. 

106`EngFormatter` Format labels in engineering notation. 

107`PercentFormatter` Format labels as a percentage. 

108========================= ===================================================== 

109 

110You can derive your own formatter from the Formatter base class by 

111simply overriding the ``__call__`` method. The formatter class has 

112access to the axis view and data limits. 

113 

114To control the major and minor tick label formats, use one of the 

115following methods:: 

116 

117 ax.xaxis.set_major_formatter(xmajor_formatter) 

118 ax.xaxis.set_minor_formatter(xminor_formatter) 

119 ax.yaxis.set_major_formatter(ymajor_formatter) 

120 ax.yaxis.set_minor_formatter(yminor_formatter) 

121 

122In addition to a `.Formatter` instance, `~.Axis.set_major_formatter` and 

123`~.Axis.set_minor_formatter` also accept a ``str`` or function. ``str`` input 

124will be internally replaced with an autogenerated `.StrMethodFormatter` with 

125the input ``str``. For function input, a `.FuncFormatter` with the input 

126function will be generated and used. 

127 

128See :doc:`/gallery/ticks/major_minor_demo` for an example of setting major 

129and minor ticks. See the :mod:`matplotlib.dates` module for more information 

130and examples of using date locators and formatters. 

131""" 

132 

133import itertools 

134import logging 

135import locale 

136import math 

137from numbers import Integral 

138import string 

139 

140import numpy as np 

141 

142import matplotlib as mpl 

143from matplotlib import _api, cbook 

144from matplotlib import transforms as mtransforms 

145 

146_log = logging.getLogger(__name__) 

147 

148__all__ = ('TickHelper', 'Formatter', 'FixedFormatter', 

149 'NullFormatter', 'FuncFormatter', 'FormatStrFormatter', 

150 'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter', 

151 'LogFormatterExponent', 'LogFormatterMathtext', 

152 'LogFormatterSciNotation', 

153 'LogitFormatter', 'EngFormatter', 'PercentFormatter', 

154 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator', 

155 'LinearLocator', 'LogLocator', 'AutoLocator', 

156 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator', 

157 'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator') 

158 

159 

160class _DummyAxis: 

161 __name__ = "dummy" 

162 

163 def __init__(self, minpos=0): 

164 self._data_interval = (0, 1) 

165 self._view_interval = (0, 1) 

166 self._minpos = minpos 

167 

168 def get_view_interval(self): 

169 return self._view_interval 

170 

171 def set_view_interval(self, vmin, vmax): 

172 self._view_interval = (vmin, vmax) 

173 

174 def get_minpos(self): 

175 return self._minpos 

176 

177 def get_data_interval(self): 

178 return self._data_interval 

179 

180 def set_data_interval(self, vmin, vmax): 

181 self._data_interval = (vmin, vmax) 

182 

183 def get_tick_space(self): 

184 # Just use the long-standing default of nbins==9 

185 return 9 

186 

187 

188class TickHelper: 

189 axis = None 

190 

191 def set_axis(self, axis): 

192 self.axis = axis 

193 

194 def create_dummy_axis(self, **kwargs): 

195 if self.axis is None: 

196 self.axis = _DummyAxis(**kwargs) 

197 

198 

199class Formatter(TickHelper): 

200 """ 

201 Create a string based on a tick value and location. 

202 """ 

203 # some classes want to see all the locs to help format 

204 # individual ones 

205 locs = [] 

206 

207 def __call__(self, x, pos=None): 

208 """ 

209 Return the format for tick value *x* at position pos. 

210 ``pos=None`` indicates an unspecified location. 

211 """ 

212 raise NotImplementedError('Derived must override') 

213 

214 def format_ticks(self, values): 

215 """Return the tick labels for all the ticks at once.""" 

216 self.set_locs(values) 

217 return [self(value, i) for i, value in enumerate(values)] 

218 

219 def format_data(self, value): 

220 """ 

221 Return the full string representation of the value with the 

222 position unspecified. 

223 """ 

224 return self.__call__(value) 

225 

226 def format_data_short(self, value): 

227 """ 

228 Return a short string version of the tick value. 

229 

230 Defaults to the position-independent long value. 

231 """ 

232 return self.format_data(value) 

233 

234 def get_offset(self): 

235 return '' 

236 

237 def set_locs(self, locs): 

238 """ 

239 Set the locations of the ticks. 

240 

241 This method is called before computing the tick labels because some 

242 formatters need to know all tick locations to do so. 

243 """ 

244 self.locs = locs 

245 

246 @staticmethod 

247 def fix_minus(s): 

248 """ 

249 Some classes may want to replace a hyphen for minus with the proper 

250 Unicode symbol (U+2212) for typographical correctness. This is a 

251 helper method to perform such a replacement when it is enabled via 

252 :rc:`axes.unicode_minus`. 

253 """ 

254 return (s.replace('-', '\N{MINUS SIGN}') 

255 if mpl.rcParams['axes.unicode_minus'] 

256 else s) 

257 

258 def _set_locator(self, locator): 

259 """Subclasses may want to override this to set a locator.""" 

260 pass 

261 

262 

263class NullFormatter(Formatter): 

264 """Always return the empty string.""" 

265 

266 def __call__(self, x, pos=None): 

267 # docstring inherited 

268 return '' 

269 

270 

271class FixedFormatter(Formatter): 

272 """ 

273 Return fixed strings for tick labels based only on position, not value. 

274 

275 .. note:: 

276 `.FixedFormatter` should only be used together with `.FixedLocator`. 

277 Otherwise, the labels may end up in unexpected positions. 

278 """ 

279 

280 def __init__(self, seq): 

281 """Set the sequence *seq* of strings that will be used for labels.""" 

282 self.seq = seq 

283 self.offset_string = '' 

284 

285 def __call__(self, x, pos=None): 

286 """ 

287 Return the label that matches the position, regardless of the value. 

288 

289 For positions ``pos < len(seq)``, return ``seq[i]`` regardless of 

290 *x*. Otherwise return empty string. ``seq`` is the sequence of 

291 strings that this object was initialized with. 

292 """ 

293 if pos is None or pos >= len(self.seq): 

294 return '' 

295 else: 

296 return self.seq[pos] 

297 

298 def get_offset(self): 

299 return self.offset_string 

300 

301 def set_offset_string(self, ofs): 

302 self.offset_string = ofs 

303 

304 

305class FuncFormatter(Formatter): 

306 """ 

307 Use a user-defined function for formatting. 

308 

309 The function should take in two inputs (a tick value ``x`` and a 

310 position ``pos``), and return a string containing the corresponding 

311 tick label. 

312 """ 

313 

314 def __init__(self, func): 

315 self.func = func 

316 self.offset_string = "" 

317 

318 def __call__(self, x, pos=None): 

319 """ 

320 Return the value of the user defined function. 

321 

322 *x* and *pos* are passed through as-is. 

323 """ 

324 return self.func(x, pos) 

325 

326 def get_offset(self): 

327 return self.offset_string 

328 

329 def set_offset_string(self, ofs): 

330 self.offset_string = ofs 

331 

332 

333class FormatStrFormatter(Formatter): 

334 """ 

335 Use an old-style ('%' operator) format string to format the tick. 

336 

337 The format string should have a single variable format (%) in it. 

338 It will be applied to the value (not the position) of the tick. 

339 

340 Negative numeric values (e.g., -1) will use a dash, not a Unicode minus; 

341 use mathtext to get a Unicode minus by wrapping the format specifier with $ 

342 (e.g. "$%g$"). 

343 """ 

344 

345 def __init__(self, fmt): 

346 self.fmt = fmt 

347 

348 def __call__(self, x, pos=None): 

349 """ 

350 Return the formatted label string. 

351 

352 Only the value *x* is formatted. The position is ignored. 

353 """ 

354 return self.fmt % x 

355 

356 

357class _UnicodeMinusFormat(string.Formatter): 

358 """ 

359 A specialized string formatter so that `.StrMethodFormatter` respects 

360 :rc:`axes.unicode_minus`. This implementation relies on the fact that the 

361 format string is only ever called with kwargs *x* and *pos*, so it blindly 

362 replaces dashes by unicode minuses without further checking. 

363 """ 

364 

365 def format_field(self, value, format_spec): 

366 return Formatter.fix_minus(super().format_field(value, format_spec)) 

367 

368 

369class StrMethodFormatter(Formatter): 

370 """ 

371 Use a new-style format string (as used by `str.format`) to format the tick. 

372 

373 The field used for the tick value must be labeled *x* and the field used 

374 for the tick position must be labeled *pos*. 

375 

376 The formatter will respect :rc:`axes.unicode_minus` when formatting 

377 negative numeric values. 

378 

379 It is typically unnecessary to explicitly construct `.StrMethodFormatter` 

380 objects, as `~.Axis.set_major_formatter` directly accepts the format string 

381 itself. 

382 """ 

383 

384 def __init__(self, fmt): 

385 self.fmt = fmt 

386 

387 def __call__(self, x, pos=None): 

388 """ 

389 Return the formatted label string. 

390 

391 *x* and *pos* are passed to `str.format` as keyword arguments 

392 with those exact names. 

393 """ 

394 return _UnicodeMinusFormat().format(self.fmt, x=x, pos=pos) 

395 

396 

397class ScalarFormatter(Formatter): 

398 """ 

399 Format tick values as a number. 

400 

401 Parameters 

402 ---------- 

403 useOffset : bool or float, default: :rc:`axes.formatter.useoffset` 

404 Whether to use offset notation. See `.set_useOffset`. 

405 useMathText : bool, default: :rc:`axes.formatter.use_mathtext` 

406 Whether to use fancy math formatting. See `.set_useMathText`. 

407 useLocale : bool, default: :rc:`axes.formatter.use_locale`. 

408 Whether to use locale settings for decimal sign and positive sign. 

409 See `.set_useLocale`. 

410 

411 Notes 

412 ----- 

413 In addition to the parameters above, the formatting of scientific vs. 

414 floating point representation can be configured via `.set_scientific` 

415 and `.set_powerlimits`). 

416 

417 **Offset notation and scientific notation** 

418 

419 Offset notation and scientific notation look quite similar at first sight. 

420 Both split some information from the formatted tick values and display it 

421 at the end of the axis. 

422 

423 - The scientific notation splits up the order of magnitude, i.e. a 

424 multiplicative scaling factor, e.g. ``1e6``. 

425 

426 - The offset notation separates an additive constant, e.g. ``+1e6``. The 

427 offset notation label is always prefixed with a ``+`` or ``-`` sign 

428 and is thus distinguishable from the order of magnitude label. 

429 

430 The following plot with x limits ``1_000_000`` to ``1_000_010`` illustrates 

431 the different formatting. Note the labels at the right edge of the x axis. 

432 

433 .. plot:: 

434 

435 lim = (1_000_000, 1_000_010) 

436 

437 fig, (ax1, ax2, ax3) = plt.subplots(3, 1, gridspec_kw={'hspace': 2}) 

438 ax1.set(title='offset notation', xlim=lim) 

439 ax2.set(title='scientific notation', xlim=lim) 

440 ax2.xaxis.get_major_formatter().set_useOffset(False) 

441 ax3.set(title='floating-point notation', xlim=lim) 

442 ax3.xaxis.get_major_formatter().set_useOffset(False) 

443 ax3.xaxis.get_major_formatter().set_scientific(False) 

444 

445 """ 

446 

447 def __init__(self, useOffset=None, useMathText=None, useLocale=None): 

448 if useOffset is None: 

449 useOffset = mpl.rcParams['axes.formatter.useoffset'] 

450 self._offset_threshold = \ 

451 mpl.rcParams['axes.formatter.offset_threshold'] 

452 self.set_useOffset(useOffset) 

453 self._usetex = mpl.rcParams['text.usetex'] 

454 self.set_useMathText(useMathText) 

455 self.orderOfMagnitude = 0 

456 self.format = '' 

457 self._scientific = True 

458 self._powerlimits = mpl.rcParams['axes.formatter.limits'] 

459 self.set_useLocale(useLocale) 

460 

461 def get_useOffset(self): 

462 """ 

463 Return whether automatic mode for offset notation is active. 

464 

465 This returns True if ``set_useOffset(True)``; it returns False if an 

466 explicit offset was set, e.g. ``set_useOffset(1000)``. 

467 

468 See Also 

469 -------- 

470 ScalarFormatter.set_useOffset 

471 """ 

472 return self._useOffset 

473 

474 def set_useOffset(self, val): 

475 """ 

476 Set whether to use offset notation. 

477 

478 When formatting a set numbers whose value is large compared to their 

479 range, the formatter can separate an additive constant. This can 

480 shorten the formatted numbers so that they are less likely to overlap 

481 when drawn on an axis. 

482 

483 Parameters 

484 ---------- 

485 val : bool or float 

486 - If False, do not use offset notation. 

487 - If True (=automatic mode), use offset notation if it can make 

488 the residual numbers significantly shorter. The exact behavior 

489 is controlled by :rc:`axes.formatter.offset_threshold`. 

490 - If a number, force an offset of the given value. 

491 

492 Examples 

493 -------- 

494 With active offset notation, the values 

495 

496 ``100_000, 100_002, 100_004, 100_006, 100_008`` 

497 

498 will be formatted as ``0, 2, 4, 6, 8`` plus an offset ``+1e5``, which 

499 is written to the edge of the axis. 

500 """ 

501 if val in [True, False]: 

502 self.offset = 0 

503 self._useOffset = val 

504 else: 

505 self._useOffset = False 

506 self.offset = val 

507 

508 useOffset = property(fget=get_useOffset, fset=set_useOffset) 

509 

510 def get_useLocale(self): 

511 """ 

512 Return whether locale settings are used for formatting. 

513 

514 See Also 

515 -------- 

516 ScalarFormatter.set_useLocale 

517 """ 

518 return self._useLocale 

519 

520 def set_useLocale(self, val): 

521 """ 

522 Set whether to use locale settings for decimal sign and positive sign. 

523 

524 Parameters 

525 ---------- 

526 val : bool or None 

527 *None* resets to :rc:`axes.formatter.use_locale`. 

528 """ 

529 if val is None: 

530 self._useLocale = mpl.rcParams['axes.formatter.use_locale'] 

531 else: 

532 self._useLocale = val 

533 

534 useLocale = property(fget=get_useLocale, fset=set_useLocale) 

535 

536 def _format_maybe_minus_and_locale(self, fmt, arg): 

537 """ 

538 Format *arg* with *fmt*, applying Unicode minus and locale if desired. 

539 """ 

540 return self.fix_minus( 

541 # Escape commas introduced by locale.format_string if using math text, 

542 # but not those present from the beginning in fmt. 

543 (",".join(locale.format_string(part, (arg,), True).replace(",", "{,}") 

544 for part in fmt.split(",")) if self._useMathText 

545 else locale.format_string(fmt, (arg,), True)) 

546 if self._useLocale 

547 else fmt % arg) 

548 

549 def get_useMathText(self): 

550 """ 

551 Return whether to use fancy math formatting. 

552 

553 See Also 

554 -------- 

555 ScalarFormatter.set_useMathText 

556 """ 

557 return self._useMathText 

558 

559 def set_useMathText(self, val): 

560 r""" 

561 Set whether to use fancy math formatting. 

562 

563 If active, scientific notation is formatted as :math:`1.2 \times 10^3`. 

564 

565 Parameters 

566 ---------- 

567 val : bool or None 

568 *None* resets to :rc:`axes.formatter.use_mathtext`. 

569 """ 

570 if val is None: 

571 self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] 

572 if self._useMathText is False: 

573 try: 

574 from matplotlib import font_manager 

575 ufont = font_manager.findfont( 

576 font_manager.FontProperties( 

577 mpl.rcParams["font.family"] 

578 ), 

579 fallback_to_default=False, 

580 ) 

581 except ValueError: 

582 ufont = None 

583 

584 if ufont == str(cbook._get_data_path("fonts/ttf/cmr10.ttf")): 

585 _api.warn_external( 

586 "cmr10 font should ideally be used with " 

587 "mathtext, set axes.formatter.use_mathtext to True" 

588 ) 

589 else: 

590 self._useMathText = val 

591 

592 useMathText = property(fget=get_useMathText, fset=set_useMathText) 

593 

594 def __call__(self, x, pos=None): 

595 """ 

596 Return the format for tick value *x* at position *pos*. 

597 """ 

598 if len(self.locs) == 0: 

599 return '' 

600 else: 

601 xp = (x - self.offset) / (10. ** self.orderOfMagnitude) 

602 if abs(xp) < 1e-8: 

603 xp = 0 

604 return self._format_maybe_minus_and_locale(self.format, xp) 

605 

606 def set_scientific(self, b): 

607 """ 

608 Turn scientific notation on or off. 

609 

610 See Also 

611 -------- 

612 ScalarFormatter.set_powerlimits 

613 """ 

614 self._scientific = bool(b) 

615 

616 def set_powerlimits(self, lims): 

617 r""" 

618 Set size thresholds for scientific notation. 

619 

620 Parameters 

621 ---------- 

622 lims : (int, int) 

623 A tuple *(min_exp, max_exp)* containing the powers of 10 that 

624 determine the switchover threshold. For a number representable as 

625 :math:`a \times 10^\mathrm{exp}` with :math:`1 <= |a| < 10`, 

626 scientific notation will be used if ``exp <= min_exp`` or 

627 ``exp >= max_exp``. 

628 

629 The default limits are controlled by :rc:`axes.formatter.limits`. 

630 

631 In particular numbers with *exp* equal to the thresholds are 

632 written in scientific notation. 

633 

634 Typically, *min_exp* will be negative and *max_exp* will be 

635 positive. 

636 

637 For example, ``formatter.set_powerlimits((-3, 4))`` will provide 

638 the following formatting: 

639 :math:`1 \times 10^{-3}, 9.9 \times 10^{-3}, 0.01,` 

640 :math:`9999, 1 \times 10^4`. 

641 

642 See Also 

643 -------- 

644 ScalarFormatter.set_scientific 

645 """ 

646 if len(lims) != 2: 

647 raise ValueError("'lims' must be a sequence of length 2") 

648 self._powerlimits = lims 

649 

650 def format_data_short(self, value): 

651 # docstring inherited 

652 if value is np.ma.masked: 

653 return "" 

654 if isinstance(value, Integral): 

655 fmt = "%d" 

656 else: 

657 if getattr(self.axis, "__name__", "") in ["xaxis", "yaxis"]: 

658 if self.axis.__name__ == "xaxis": 

659 axis_trf = self.axis.axes.get_xaxis_transform() 

660 axis_inv_trf = axis_trf.inverted() 

661 screen_xy = axis_trf.transform((value, 0)) 

662 neighbor_values = axis_inv_trf.transform( 

663 screen_xy + [[-1, 0], [+1, 0]])[:, 0] 

664 else: # yaxis: 

665 axis_trf = self.axis.axes.get_yaxis_transform() 

666 axis_inv_trf = axis_trf.inverted() 

667 screen_xy = axis_trf.transform((0, value)) 

668 neighbor_values = axis_inv_trf.transform( 

669 screen_xy + [[0, -1], [0, +1]])[:, 1] 

670 delta = abs(neighbor_values - value).max() 

671 else: 

672 # Rough approximation: no more than 1e4 divisions. 

673 a, b = self.axis.get_view_interval() 

674 delta = (b - a) / 1e4 

675 fmt = f"%-#.{cbook._g_sig_digits(value, delta)}g" 

676 return self._format_maybe_minus_and_locale(fmt, value) 

677 

678 def format_data(self, value): 

679 # docstring inherited 

680 e = math.floor(math.log10(abs(value))) 

681 s = round(value / 10**e, 10) 

682 significand = self._format_maybe_minus_and_locale( 

683 "%d" if s % 1 == 0 else "%1.10g", s) 

684 if e == 0: 

685 return significand 

686 exponent = self._format_maybe_minus_and_locale("%d", e) 

687 if self._useMathText or self._usetex: 

688 exponent = "10^{%s}" % exponent 

689 return (exponent if s == 1 # reformat 1x10^y as 10^y 

690 else rf"{significand} \times {exponent}") 

691 else: 

692 return f"{significand}e{exponent}" 

693 

694 def get_offset(self): 

695 """ 

696 Return scientific notation, plus offset. 

697 """ 

698 if len(self.locs) == 0: 

699 return '' 

700 if self.orderOfMagnitude or self.offset: 

701 offsetStr = '' 

702 sciNotStr = '' 

703 if self.offset: 

704 offsetStr = self.format_data(self.offset) 

705 if self.offset > 0: 

706 offsetStr = '+' + offsetStr 

707 if self.orderOfMagnitude: 

708 if self._usetex or self._useMathText: 

709 sciNotStr = self.format_data(10 ** self.orderOfMagnitude) 

710 else: 

711 sciNotStr = '1e%d' % self.orderOfMagnitude 

712 if self._useMathText or self._usetex: 

713 if sciNotStr != '': 

714 sciNotStr = r'\times\mathdefault{%s}' % sciNotStr 

715 s = fr'${sciNotStr}\mathdefault{{{offsetStr}}}$' 

716 else: 

717 s = ''.join((sciNotStr, offsetStr)) 

718 return self.fix_minus(s) 

719 return '' 

720 

721 def set_locs(self, locs): 

722 # docstring inherited 

723 self.locs = locs 

724 if len(self.locs) > 0: 

725 if self._useOffset: 

726 self._compute_offset() 

727 self._set_order_of_magnitude() 

728 self._set_format() 

729 

730 def _compute_offset(self): 

731 locs = self.locs 

732 # Restrict to visible ticks. 

733 vmin, vmax = sorted(self.axis.get_view_interval()) 

734 locs = np.asarray(locs) 

735 locs = locs[(vmin <= locs) & (locs <= vmax)] 

736 if not len(locs): 

737 self.offset = 0 

738 return 

739 lmin, lmax = locs.min(), locs.max() 

740 # Only use offset if there are at least two ticks and every tick has 

741 # the same sign. 

742 if lmin == lmax or lmin <= 0 <= lmax: 

743 self.offset = 0 

744 return 

745 # min, max comparing absolute values (we want division to round towards 

746 # zero so we work on absolute values). 

747 abs_min, abs_max = sorted([abs(float(lmin)), abs(float(lmax))]) 

748 sign = math.copysign(1, lmin) 

749 # What is the smallest power of ten such that abs_min and abs_max are 

750 # equal up to that precision? 

751 # Note: Internally using oom instead of 10 ** oom avoids some numerical 

752 # accuracy issues. 

753 oom_max = np.ceil(math.log10(abs_max)) 

754 oom = 1 + next(oom for oom in itertools.count(oom_max, -1) 

755 if abs_min // 10 ** oom != abs_max // 10 ** oom) 

756 if (abs_max - abs_min) / 10 ** oom <= 1e-2: 

757 # Handle the case of straddling a multiple of a large power of ten 

758 # (relative to the span). 

759 # What is the smallest power of ten such that abs_min and abs_max 

760 # are no more than 1 apart at that precision? 

761 oom = 1 + next(oom for oom in itertools.count(oom_max, -1) 

762 if abs_max // 10 ** oom - abs_min // 10 ** oom > 1) 

763 # Only use offset if it saves at least _offset_threshold digits. 

764 n = self._offset_threshold - 1 

765 self.offset = (sign * (abs_max // 10 ** oom) * 10 ** oom 

766 if abs_max // 10 ** oom >= 10**n 

767 else 0) 

768 

769 def _set_order_of_magnitude(self): 

770 # if scientific notation is to be used, find the appropriate exponent 

771 # if using a numerical offset, find the exponent after applying the 

772 # offset. When lower power limit = upper <> 0, use provided exponent. 

773 if not self._scientific: 

774 self.orderOfMagnitude = 0 

775 return 

776 if self._powerlimits[0] == self._powerlimits[1] != 0: 

777 # fixed scaling when lower power limit = upper <> 0. 

778 self.orderOfMagnitude = self._powerlimits[0] 

779 return 

780 # restrict to visible ticks 

781 vmin, vmax = sorted(self.axis.get_view_interval()) 

782 locs = np.asarray(self.locs) 

783 locs = locs[(vmin <= locs) & (locs <= vmax)] 

784 locs = np.abs(locs) 

785 if not len(locs): 

786 self.orderOfMagnitude = 0 

787 return 

788 if self.offset: 

789 oom = math.floor(math.log10(vmax - vmin)) 

790 else: 

791 val = locs.max() 

792 if val == 0: 

793 oom = 0 

794 else: 

795 oom = math.floor(math.log10(val)) 

796 if oom <= self._powerlimits[0]: 

797 self.orderOfMagnitude = oom 

798 elif oom >= self._powerlimits[1]: 

799 self.orderOfMagnitude = oom 

800 else: 

801 self.orderOfMagnitude = 0 

802 

803 def _set_format(self): 

804 # set the format string to format all the ticklabels 

805 if len(self.locs) < 2: 

806 # Temporarily augment the locations with the axis end points. 

807 _locs = [*self.locs, *self.axis.get_view_interval()] 

808 else: 

809 _locs = self.locs 

810 locs = (np.asarray(_locs) - self.offset) / 10. ** self.orderOfMagnitude 

811 loc_range = np.ptp(locs) 

812 # Curvilinear coordinates can yield two identical points. 

813 if loc_range == 0: 

814 loc_range = np.max(np.abs(locs)) 

815 # Both points might be zero. 

816 if loc_range == 0: 

817 loc_range = 1 

818 if len(self.locs) < 2: 

819 # We needed the end points only for the loc_range calculation. 

820 locs = locs[:-2] 

821 loc_range_oom = int(math.floor(math.log10(loc_range))) 

822 # first estimate: 

823 sigfigs = max(0, 3 - loc_range_oom) 

824 # refined estimate: 

825 thresh = 1e-3 * 10 ** loc_range_oom 

826 while sigfigs >= 0: 

827 if np.abs(locs - np.round(locs, decimals=sigfigs)).max() < thresh: 

828 sigfigs -= 1 

829 else: 

830 break 

831 sigfigs += 1 

832 self.format = f'%1.{sigfigs}f' 

833 if self._usetex or self._useMathText: 

834 self.format = r'$\mathdefault{%s}$' % self.format 

835 

836 

837class LogFormatter(Formatter): 

838 """ 

839 Base class for formatting ticks on a log or symlog scale. 

840 

841 It may be instantiated directly, or subclassed. 

842 

843 Parameters 

844 ---------- 

845 base : float, default: 10. 

846 Base of the logarithm used in all calculations. 

847 

848 labelOnlyBase : bool, default: False 

849 If True, label ticks only at integer powers of base. 

850 This is normally True for major ticks and False for 

851 minor ticks. 

852 

853 minor_thresholds : (subset, all), default: (1, 0.4) 

854 If labelOnlyBase is False, these two numbers control 

855 the labeling of ticks that are not at integer powers of 

856 base; normally these are the minor ticks. The controlling 

857 parameter is the log of the axis data range. In the typical 

858 case where base is 10 it is the number of decades spanned 

859 by the axis, so we can call it 'numdec'. If ``numdec <= all``, 

860 all minor ticks will be labeled. If ``all < numdec <= subset``, 

861 then only a subset of minor ticks will be labeled, so as to 

862 avoid crowding. If ``numdec > subset`` then no minor ticks will 

863 be labeled. 

864 

865 linthresh : None or float, default: None 

866 If a symmetric log scale is in use, its ``linthresh`` 

867 parameter must be supplied here. 

868 

869 Notes 

870 ----- 

871 The `set_locs` method must be called to enable the subsetting 

872 logic controlled by the ``minor_thresholds`` parameter. 

873 

874 In some cases such as the colorbar, there is no distinction between 

875 major and minor ticks; the tick locations might be set manually, 

876 or by a locator that puts ticks at integer powers of base and 

877 at intermediate locations. For this situation, disable the 

878 minor_thresholds logic by using ``minor_thresholds=(np.inf, np.inf)``, 

879 so that all ticks will be labeled. 

880 

881 To disable labeling of minor ticks when 'labelOnlyBase' is False, 

882 use ``minor_thresholds=(0, 0)``. This is the default for the 

883 "classic" style. 

884 

885 Examples 

886 -------- 

887 To label a subset of minor ticks when the view limits span up 

888 to 2 decades, and all of the ticks when zoomed in to 0.5 decades 

889 or less, use ``minor_thresholds=(2, 0.5)``. 

890 

891 To label all minor ticks when the view limits span up to 1.5 

892 decades, use ``minor_thresholds=(1.5, 1.5)``. 

893 """ 

894 

895 def __init__(self, base=10.0, labelOnlyBase=False, 

896 minor_thresholds=None, 

897 linthresh=None): 

898 

899 self.set_base(base) 

900 self.set_label_minor(labelOnlyBase) 

901 if minor_thresholds is None: 

902 if mpl.rcParams['_internal.classic_mode']: 

903 minor_thresholds = (0, 0) 

904 else: 

905 minor_thresholds = (1, 0.4) 

906 self.minor_thresholds = minor_thresholds 

907 self._sublabels = None 

908 self._linthresh = linthresh 

909 

910 def set_base(self, base): 

911 """ 

912 Change the *base* for labeling. 

913 

914 .. warning:: 

915 Should always match the base used for :class:`LogLocator` 

916 """ 

917 self._base = float(base) 

918 

919 def set_label_minor(self, labelOnlyBase): 

920 """ 

921 Switch minor tick labeling on or off. 

922 

923 Parameters 

924 ---------- 

925 labelOnlyBase : bool 

926 If True, label ticks only at integer powers of base. 

927 """ 

928 self.labelOnlyBase = labelOnlyBase 

929 

930 def set_locs(self, locs=None): 

931 """ 

932 Use axis view limits to control which ticks are labeled. 

933 

934 The *locs* parameter is ignored in the present algorithm. 

935 """ 

936 if np.isinf(self.minor_thresholds[0]): 

937 self._sublabels = None 

938 return 

939 

940 # Handle symlog case: 

941 linthresh = self._linthresh 

942 if linthresh is None: 

943 try: 

944 linthresh = self.axis.get_transform().linthresh 

945 except AttributeError: 

946 pass 

947 

948 vmin, vmax = self.axis.get_view_interval() 

949 if vmin > vmax: 

950 vmin, vmax = vmax, vmin 

951 

952 if linthresh is None and vmin <= 0: 

953 # It's probably a colorbar with 

954 # a format kwarg setting a LogFormatter in the manner 

955 # that worked with 1.5.x, but that doesn't work now. 

956 self._sublabels = {1} # label powers of base 

957 return 

958 

959 b = self._base 

960 if linthresh is not None: # symlog 

961 # Only compute the number of decades in the logarithmic part of the 

962 # axis 

963 numdec = 0 

964 if vmin < -linthresh: 

965 rhs = min(vmax, -linthresh) 

966 numdec += math.log(vmin / rhs) / math.log(b) 

967 if vmax > linthresh: 

968 lhs = max(vmin, linthresh) 

969 numdec += math.log(vmax / lhs) / math.log(b) 

970 else: 

971 vmin = math.log(vmin) / math.log(b) 

972 vmax = math.log(vmax) / math.log(b) 

973 numdec = abs(vmax - vmin) 

974 

975 if numdec > self.minor_thresholds[0]: 

976 # Label only bases 

977 self._sublabels = {1} 

978 elif numdec > self.minor_thresholds[1]: 

979 # Add labels between bases at log-spaced coefficients; 

980 # include base powers in case the locations include 

981 # "major" and "minor" points, as in colorbar. 

982 c = np.geomspace(1, b, int(b)//2 + 1) 

983 self._sublabels = set(np.round(c)) 

984 # For base 10, this yields (1, 2, 3, 4, 6, 10). 

985 else: 

986 # Label all integer multiples of base**n. 

987 self._sublabels = set(np.arange(1, b + 1)) 

988 

989 def _num_to_string(self, x, vmin, vmax): 

990 if x > 10000: 

991 s = '%1.0e' % x 

992 elif x < 1: 

993 s = '%1.0e' % x 

994 else: 

995 s = self._pprint_val(x, vmax - vmin) 

996 return s 

997 

998 def __call__(self, x, pos=None): 

999 # docstring inherited 

1000 if x == 0.0: # Symlog 

1001 return '0' 

1002 

1003 x = abs(x) 

1004 b = self._base 

1005 # only label the decades 

1006 fx = math.log(x) / math.log(b) 

1007 is_x_decade = _is_close_to_int(fx) 

1008 exponent = round(fx) if is_x_decade else np.floor(fx) 

1009 coeff = round(b ** (fx - exponent)) 

1010 

1011 if self.labelOnlyBase and not is_x_decade: 

1012 return '' 

1013 if self._sublabels is not None and coeff not in self._sublabels: 

1014 return '' 

1015 

1016 vmin, vmax = self.axis.get_view_interval() 

1017 vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) 

1018 s = self._num_to_string(x, vmin, vmax) 

1019 return self.fix_minus(s) 

1020 

1021 def format_data(self, value): 

1022 with cbook._setattr_cm(self, labelOnlyBase=False): 

1023 return cbook.strip_math(self.__call__(value)) 

1024 

1025 def format_data_short(self, value): 

1026 # docstring inherited 

1027 return ('%-12g' % value).rstrip() 

1028 

1029 def _pprint_val(self, x, d): 

1030 # If the number is not too big and it's an int, format it as an int. 

1031 if abs(x) < 1e4 and x == int(x): 

1032 return '%d' % x 

1033 fmt = ('%1.3e' if d < 1e-2 else 

1034 '%1.3f' if d <= 1 else 

1035 '%1.2f' if d <= 10 else 

1036 '%1.1f' if d <= 1e5 else 

1037 '%1.1e') 

1038 s = fmt % x 

1039 tup = s.split('e') 

1040 if len(tup) == 2: 

1041 mantissa = tup[0].rstrip('0').rstrip('.') 

1042 exponent = int(tup[1]) 

1043 if exponent: 

1044 s = '%se%d' % (mantissa, exponent) 

1045 else: 

1046 s = mantissa 

1047 else: 

1048 s = s.rstrip('0').rstrip('.') 

1049 return s 

1050 

1051 

1052class LogFormatterExponent(LogFormatter): 

1053 """ 

1054 Format values for log axis using ``exponent = log_base(value)``. 

1055 """ 

1056 def _num_to_string(self, x, vmin, vmax): 

1057 fx = math.log(x) / math.log(self._base) 

1058 if abs(fx) > 10000: 

1059 s = '%1.0g' % fx 

1060 elif abs(fx) < 1: 

1061 s = '%1.0g' % fx 

1062 else: 

1063 fd = math.log(vmax - vmin) / math.log(self._base) 

1064 s = self._pprint_val(fx, fd) 

1065 return s 

1066 

1067 

1068class LogFormatterMathtext(LogFormatter): 

1069 """ 

1070 Format values for log axis using ``exponent = log_base(value)``. 

1071 """ 

1072 

1073 def _non_decade_format(self, sign_string, base, fx, usetex): 

1074 """Return string for non-decade locations.""" 

1075 return r'$\mathdefault{%s%s^{%.2f}}$' % (sign_string, base, fx) 

1076 

1077 def __call__(self, x, pos=None): 

1078 # docstring inherited 

1079 if x == 0: # Symlog 

1080 return r'$\mathdefault{0}$' 

1081 

1082 sign_string = '-' if x < 0 else '' 

1083 x = abs(x) 

1084 b = self._base 

1085 

1086 # only label the decades 

1087 fx = math.log(x) / math.log(b) 

1088 is_x_decade = _is_close_to_int(fx) 

1089 exponent = round(fx) if is_x_decade else np.floor(fx) 

1090 coeff = round(b ** (fx - exponent)) 

1091 

1092 if self.labelOnlyBase and not is_x_decade: 

1093 return '' 

1094 if self._sublabels is not None and coeff not in self._sublabels: 

1095 return '' 

1096 

1097 if is_x_decade: 

1098 fx = round(fx) 

1099 

1100 # use string formatting of the base if it is not an integer 

1101 if b % 1 == 0.0: 

1102 base = '%d' % b 

1103 else: 

1104 base = '%s' % b 

1105 

1106 if abs(fx) < mpl.rcParams['axes.formatter.min_exponent']: 

1107 return r'$\mathdefault{%s%g}$' % (sign_string, x) 

1108 elif not is_x_decade: 

1109 usetex = mpl.rcParams['text.usetex'] 

1110 return self._non_decade_format(sign_string, base, fx, usetex) 

1111 else: 

1112 return r'$\mathdefault{%s%s^{%d}}$' % (sign_string, base, fx) 

1113 

1114 

1115class LogFormatterSciNotation(LogFormatterMathtext): 

1116 """ 

1117 Format values following scientific notation in a logarithmic axis. 

1118 """ 

1119 

1120 def _non_decade_format(self, sign_string, base, fx, usetex): 

1121 """Return string for non-decade locations.""" 

1122 b = float(base) 

1123 exponent = math.floor(fx) 

1124 coeff = b ** (fx - exponent) 

1125 if _is_close_to_int(coeff): 

1126 coeff = round(coeff) 

1127 return r'$\mathdefault{%s%g\times%s^{%d}}$' \ 

1128 % (sign_string, coeff, base, exponent) 

1129 

1130 

1131class LogitFormatter(Formatter): 

1132 """ 

1133 Probability formatter (using Math text). 

1134 """ 

1135 

1136 def __init__( 

1137 self, 

1138 *, 

1139 use_overline=False, 

1140 one_half=r"\frac{1}{2}", 

1141 minor=False, 

1142 minor_threshold=25, 

1143 minor_number=6, 

1144 ): 

1145 r""" 

1146 Parameters 

1147 ---------- 

1148 use_overline : bool, default: False 

1149 If x > 1/2, with x = 1 - v, indicate if x should be displayed as 

1150 $\overline{v}$. The default is to display $1 - v$. 

1151 

1152 one_half : str, default: r"\\frac{1}{2}" 

1153 The string used to represent 1/2. 

1154 

1155 minor : bool, default: False 

1156 Indicate if the formatter is formatting minor ticks or not. 

1157 Basically minor ticks are not labelled, except when only few ticks 

1158 are provided, ticks with most space with neighbor ticks are 

1159 labelled. See other parameters to change the default behavior. 

1160 

1161 minor_threshold : int, default: 25 

1162 Maximum number of locs for labelling some minor ticks. This 

1163 parameter have no effect if minor is False. 

1164 

1165 minor_number : int, default: 6 

1166 Number of ticks which are labelled when the number of ticks is 

1167 below the threshold. 

1168 """ 

1169 self._use_overline = use_overline 

1170 self._one_half = one_half 

1171 self._minor = minor 

1172 self._labelled = set() 

1173 self._minor_threshold = minor_threshold 

1174 self._minor_number = minor_number 

1175 

1176 def use_overline(self, use_overline): 

1177 r""" 

1178 Switch display mode with overline for labelling p>1/2. 

1179 

1180 Parameters 

1181 ---------- 

1182 use_overline : bool 

1183 If x > 1/2, with x = 1 - v, indicate if x should be displayed as 

1184 $\overline{v}$. The default is to display $1 - v$. 

1185 """ 

1186 self._use_overline = use_overline 

1187 

1188 def set_one_half(self, one_half): 

1189 r""" 

1190 Set the way one half is displayed. 

1191 

1192 one_half : str 

1193 The string used to represent 1/2. 

1194 """ 

1195 self._one_half = one_half 

1196 

1197 def set_minor_threshold(self, minor_threshold): 

1198 """ 

1199 Set the threshold for labelling minors ticks. 

1200 

1201 Parameters 

1202 ---------- 

1203 minor_threshold : int 

1204 Maximum number of locations for labelling some minor ticks. This 

1205 parameter have no effect if minor is False. 

1206 """ 

1207 self._minor_threshold = minor_threshold 

1208 

1209 def set_minor_number(self, minor_number): 

1210 """ 

1211 Set the number of minor ticks to label when some minor ticks are 

1212 labelled. 

1213 

1214 Parameters 

1215 ---------- 

1216 minor_number : int 

1217 Number of ticks which are labelled when the number of ticks is 

1218 below the threshold. 

1219 """ 

1220 self._minor_number = minor_number 

1221 

1222 def set_locs(self, locs): 

1223 self.locs = np.array(locs) 

1224 self._labelled.clear() 

1225 

1226 if not self._minor: 

1227 return None 

1228 if all( 

1229 _is_decade(x, rtol=1e-7) 

1230 or _is_decade(1 - x, rtol=1e-7) 

1231 or (_is_close_to_int(2 * x) and 

1232 int(np.round(2 * x)) == 1) 

1233 for x in locs 

1234 ): 

1235 # minor ticks are subsample from ideal, so no label 

1236 return None 

1237 if len(locs) < self._minor_threshold: 

1238 if len(locs) < self._minor_number: 

1239 self._labelled.update(locs) 

1240 else: 

1241 # we do not have a lot of minor ticks, so only few decades are 

1242 # displayed, then we choose some (spaced) minor ticks to label. 

1243 # Only minor ticks are known, we assume it is sufficient to 

1244 # choice which ticks are displayed. 

1245 # For each ticks we compute the distance between the ticks and 

1246 # the previous, and between the ticks and the next one. Ticks 

1247 # with smallest minimum are chosen. As tiebreak, the ticks 

1248 # with smallest sum is chosen. 

1249 diff = np.diff(-np.log(1 / self.locs - 1)) 

1250 space_pessimistic = np.minimum( 

1251 np.concatenate(((np.inf,), diff)), 

1252 np.concatenate((diff, (np.inf,))), 

1253 ) 

1254 space_sum = ( 

1255 np.concatenate(((0,), diff)) 

1256 + np.concatenate((diff, (0,))) 

1257 ) 

1258 good_minor = sorted( 

1259 range(len(self.locs)), 

1260 key=lambda i: (space_pessimistic[i], space_sum[i]), 

1261 )[-self._minor_number:] 

1262 self._labelled.update(locs[i] for i in good_minor) 

1263 

1264 def _format_value(self, x, locs, sci_notation=True): 

1265 if sci_notation: 

1266 exponent = math.floor(np.log10(x)) 

1267 min_precision = 0 

1268 else: 

1269 exponent = 0 

1270 min_precision = 1 

1271 value = x * 10 ** (-exponent) 

1272 if len(locs) < 2: 

1273 precision = min_precision 

1274 else: 

1275 diff = np.sort(np.abs(locs - x))[1] 

1276 precision = -np.log10(diff) + exponent 

1277 precision = ( 

1278 int(np.round(precision)) 

1279 if _is_close_to_int(precision) 

1280 else math.ceil(precision) 

1281 ) 

1282 if precision < min_precision: 

1283 precision = min_precision 

1284 mantissa = r"%.*f" % (precision, value) 

1285 if not sci_notation: 

1286 return mantissa 

1287 s = r"%s\cdot10^{%d}" % (mantissa, exponent) 

1288 return s 

1289 

1290 def _one_minus(self, s): 

1291 if self._use_overline: 

1292 return r"\overline{%s}" % s 

1293 else: 

1294 return f"1-{s}" 

1295 

1296 def __call__(self, x, pos=None): 

1297 if self._minor and x not in self._labelled: 

1298 return "" 

1299 if x <= 0 or x >= 1: 

1300 return "" 

1301 if _is_close_to_int(2 * x) and round(2 * x) == 1: 

1302 s = self._one_half 

1303 elif x < 0.5 and _is_decade(x, rtol=1e-7): 

1304 exponent = round(math.log10(x)) 

1305 s = "10^{%d}" % exponent 

1306 elif x > 0.5 and _is_decade(1 - x, rtol=1e-7): 

1307 exponent = round(math.log10(1 - x)) 

1308 s = self._one_minus("10^{%d}" % exponent) 

1309 elif x < 0.1: 

1310 s = self._format_value(x, self.locs) 

1311 elif x > 0.9: 

1312 s = self._one_minus(self._format_value(1-x, 1-self.locs)) 

1313 else: 

1314 s = self._format_value(x, self.locs, sci_notation=False) 

1315 return r"$\mathdefault{%s}$" % s 

1316 

1317 def format_data_short(self, value): 

1318 # docstring inherited 

1319 # Thresholds chosen to use scientific notation iff exponent <= -2. 

1320 if value < 0.1: 

1321 return f"{value:e}" 

1322 if value < 0.9: 

1323 return f"{value:f}" 

1324 return f"1-{1 - value:e}" 

1325 

1326 

1327class EngFormatter(Formatter): 

1328 """ 

1329 Format axis values using engineering prefixes to represent powers 

1330 of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7. 

1331 """ 

1332 

1333 # The SI engineering prefixes 

1334 ENG_PREFIXES = { 

1335 -30: "q", 

1336 -27: "r", 

1337 -24: "y", 

1338 -21: "z", 

1339 -18: "a", 

1340 -15: "f", 

1341 -12: "p", 

1342 -9: "n", 

1343 -6: "\N{MICRO SIGN}", 

1344 -3: "m", 

1345 0: "", 

1346 3: "k", 

1347 6: "M", 

1348 9: "G", 

1349 12: "T", 

1350 15: "P", 

1351 18: "E", 

1352 21: "Z", 

1353 24: "Y", 

1354 27: "R", 

1355 30: "Q" 

1356 } 

1357 

1358 def __init__(self, unit="", places=None, sep=" ", *, usetex=None, 

1359 useMathText=None): 

1360 r""" 

1361 Parameters 

1362 ---------- 

1363 unit : str, default: "" 

1364 Unit symbol to use, suitable for use with single-letter 

1365 representations of powers of 1000. For example, 'Hz' or 'm'. 

1366 

1367 places : int, default: None 

1368 Precision with which to display the number, specified in 

1369 digits after the decimal point (there will be between one 

1370 and three digits before the decimal point). If it is None, 

1371 the formatting falls back to the floating point format '%g', 

1372 which displays up to 6 *significant* digits, i.e. the equivalent 

1373 value for *places* varies between 0 and 5 (inclusive). 

1374 

1375 sep : str, default: " " 

1376 Separator used between the value and the prefix/unit. For 

1377 example, one get '3.14 mV' if ``sep`` is " " (default) and 

1378 '3.14mV' if ``sep`` is "". Besides the default behavior, some 

1379 other useful options may be: 

1380 

1381 * ``sep=""`` to append directly the prefix/unit to the value; 

1382 * ``sep="\N{THIN SPACE}"`` (``U+2009``); 

1383 * ``sep="\N{NARROW NO-BREAK SPACE}"`` (``U+202F``); 

1384 * ``sep="\N{NO-BREAK SPACE}"`` (``U+00A0``). 

1385 

1386 usetex : bool, default: :rc:`text.usetex` 

1387 To enable/disable the use of TeX's math mode for rendering the 

1388 numbers in the formatter. 

1389 

1390 useMathText : bool, default: :rc:`axes.formatter.use_mathtext` 

1391 To enable/disable the use mathtext for rendering the numbers in 

1392 the formatter. 

1393 """ 

1394 self.unit = unit 

1395 self.places = places 

1396 self.sep = sep 

1397 self.set_usetex(usetex) 

1398 self.set_useMathText(useMathText) 

1399 

1400 def get_usetex(self): 

1401 return self._usetex 

1402 

1403 def set_usetex(self, val): 

1404 if val is None: 

1405 self._usetex = mpl.rcParams['text.usetex'] 

1406 else: 

1407 self._usetex = val 

1408 

1409 usetex = property(fget=get_usetex, fset=set_usetex) 

1410 

1411 def get_useMathText(self): 

1412 return self._useMathText 

1413 

1414 def set_useMathText(self, val): 

1415 if val is None: 

1416 self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] 

1417 else: 

1418 self._useMathText = val 

1419 

1420 useMathText = property(fget=get_useMathText, fset=set_useMathText) 

1421 

1422 def __call__(self, x, pos=None): 

1423 s = f"{self.format_eng(x)}{self.unit}" 

1424 # Remove the trailing separator when there is neither prefix nor unit 

1425 if self.sep and s.endswith(self.sep): 

1426 s = s[:-len(self.sep)] 

1427 return self.fix_minus(s) 

1428 

1429 def format_eng(self, num): 

1430 """ 

1431 Format a number in engineering notation, appending a letter 

1432 representing the power of 1000 of the original number. 

1433 Some examples: 

1434 

1435 >>> format_eng(0) # for self.places = 0 

1436 '0' 

1437 

1438 >>> format_eng(1000000) # for self.places = 1 

1439 '1.0 M' 

1440 

1441 >>> format_eng(-1e-6) # for self.places = 2 

1442 '-1.00 \N{MICRO SIGN}' 

1443 """ 

1444 sign = 1 

1445 fmt = "g" if self.places is None else f".{self.places:d}f" 

1446 

1447 if num < 0: 

1448 sign = -1 

1449 num = -num 

1450 

1451 if num != 0: 

1452 pow10 = int(math.floor(math.log10(num) / 3) * 3) 

1453 else: 

1454 pow10 = 0 

1455 # Force num to zero, to avoid inconsistencies like 

1456 # format_eng(-0) = "0" and format_eng(0.0) = "0" 

1457 # but format_eng(-0.0) = "-0.0" 

1458 num = 0.0 

1459 

1460 pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) 

1461 

1462 mant = sign * num / (10.0 ** pow10) 

1463 # Taking care of the cases like 999.9..., which may be rounded to 1000 

1464 # instead of 1 k. Beware of the corner case of values that are beyond 

1465 # the range of SI prefixes (i.e. > 'Y'). 

1466 if (abs(float(format(mant, fmt))) >= 1000 

1467 and pow10 < max(self.ENG_PREFIXES)): 

1468 mant /= 1000 

1469 pow10 += 3 

1470 

1471 prefix = self.ENG_PREFIXES[int(pow10)] 

1472 if self._usetex or self._useMathText: 

1473 formatted = f"${mant:{fmt}}${self.sep}{prefix}" 

1474 else: 

1475 formatted = f"{mant:{fmt}}{self.sep}{prefix}" 

1476 

1477 return formatted 

1478 

1479 

1480class PercentFormatter(Formatter): 

1481 """ 

1482 Format numbers as a percentage. 

1483 

1484 Parameters 

1485 ---------- 

1486 xmax : float 

1487 Determines how the number is converted into a percentage. 

1488 *xmax* is the data value that corresponds to 100%. 

1489 Percentages are computed as ``x / xmax * 100``. So if the data is 

1490 already scaled to be percentages, *xmax* will be 100. Another common 

1491 situation is where *xmax* is 1.0. 

1492 

1493 decimals : None or int 

1494 The number of decimal places to place after the point. 

1495 If *None* (the default), the number will be computed automatically. 

1496 

1497 symbol : str or None 

1498 A string that will be appended to the label. It may be 

1499 *None* or empty to indicate that no symbol should be used. LaTeX 

1500 special characters are escaped in *symbol* whenever latex mode is 

1501 enabled, unless *is_latex* is *True*. 

1502 

1503 is_latex : bool 

1504 If *False*, reserved LaTeX characters in *symbol* will be escaped. 

1505 """ 

1506 def __init__(self, xmax=100, decimals=None, symbol='%', is_latex=False): 

1507 self.xmax = xmax + 0.0 

1508 self.decimals = decimals 

1509 self._symbol = symbol 

1510 self._is_latex = is_latex 

1511 

1512 def __call__(self, x, pos=None): 

1513 """Format the tick as a percentage with the appropriate scaling.""" 

1514 ax_min, ax_max = self.axis.get_view_interval() 

1515 display_range = abs(ax_max - ax_min) 

1516 return self.fix_minus(self.format_pct(x, display_range)) 

1517 

1518 def format_pct(self, x, display_range): 

1519 """ 

1520 Format the number as a percentage number with the correct 

1521 number of decimals and adds the percent symbol, if any. 

1522 

1523 If ``self.decimals`` is `None`, the number of digits after the 

1524 decimal point is set based on the *display_range* of the axis 

1525 as follows: 

1526 

1527 ============= ======== ======================= 

1528 display_range decimals sample 

1529 ============= ======== ======================= 

1530 >50 0 ``x = 34.5`` => 35% 

1531 >5 1 ``x = 34.5`` => 34.5% 

1532 >0.5 2 ``x = 34.5`` => 34.50% 

1533 ... ... ... 

1534 ============= ======== ======================= 

1535 

1536 This method will not be very good for tiny axis ranges or 

1537 extremely large ones. It assumes that the values on the chart 

1538 are percentages displayed on a reasonable scale. 

1539 """ 

1540 x = self.convert_to_pct(x) 

1541 if self.decimals is None: 

1542 # conversion works because display_range is a difference 

1543 scaled_range = self.convert_to_pct(display_range) 

1544 if scaled_range <= 0: 

1545 decimals = 0 

1546 else: 

1547 # Luckily Python's built-in ceil rounds to +inf, not away from 

1548 # zero. This is very important since the equation for decimals 

1549 # starts out as `scaled_range > 0.5 * 10**(2 - decimals)` 

1550 # and ends up with `decimals > 2 - log10(2 * scaled_range)`. 

1551 decimals = math.ceil(2.0 - math.log10(2.0 * scaled_range)) 

1552 if decimals > 5: 

1553 decimals = 5 

1554 elif decimals < 0: 

1555 decimals = 0 

1556 else: 

1557 decimals = self.decimals 

1558 s = f'{x:0.{int(decimals)}f}' 

1559 

1560 return s + self.symbol 

1561 

1562 def convert_to_pct(self, x): 

1563 return 100.0 * (x / self.xmax) 

1564 

1565 @property 

1566 def symbol(self): 

1567 r""" 

1568 The configured percent symbol as a string. 

1569 

1570 If LaTeX is enabled via :rc:`text.usetex`, the special characters 

1571 ``{'#', '$', '%', '&', '~', '_', '^', '\', '{', '}'}`` are 

1572 automatically escaped in the string. 

1573 """ 

1574 symbol = self._symbol 

1575 if not symbol: 

1576 symbol = '' 

1577 elif not self._is_latex and mpl.rcParams['text.usetex']: 

1578 # Source: http://www.personal.ceu.hu/tex/specchar.htm 

1579 # Backslash must be first for this to work correctly since 

1580 # it keeps getting added in 

1581 for spec in r'\#$%&~_^{}': 

1582 symbol = symbol.replace(spec, '\\' + spec) 

1583 return symbol 

1584 

1585 @symbol.setter 

1586 def symbol(self, symbol): 

1587 self._symbol = symbol 

1588 

1589 

1590class Locator(TickHelper): 

1591 """ 

1592 Determine tick locations. 

1593 

1594 Note that the same locator should not be used across multiple 

1595 `~matplotlib.axis.Axis` because the locator stores references to the Axis 

1596 data and view limits. 

1597 """ 

1598 

1599 # Some automatic tick locators can generate so many ticks they 

1600 # kill the machine when you try and render them. 

1601 # This parameter is set to cause locators to raise an error if too 

1602 # many ticks are generated. 

1603 MAXTICKS = 1000 

1604 

1605 def tick_values(self, vmin, vmax): 

1606 """ 

1607 Return the values of the located ticks given **vmin** and **vmax**. 

1608 

1609 .. note:: 

1610 To get tick locations with the vmin and vmax values defined 

1611 automatically for the associated ``axis`` simply call 

1612 the Locator instance:: 

1613 

1614 >>> print(type(loc)) 

1615 <type 'Locator'> 

1616 >>> print(loc()) 

1617 [1, 2, 3, 4] 

1618 

1619 """ 

1620 raise NotImplementedError('Derived must override') 

1621 

1622 def set_params(self, **kwargs): 

1623 """ 

1624 Do nothing, and raise a warning. Any locator class not supporting the 

1625 set_params() function will call this. 

1626 """ 

1627 _api.warn_external( 

1628 "'set_params()' not defined for locator of type " + 

1629 str(type(self))) 

1630 

1631 def __call__(self): 

1632 """Return the locations of the ticks.""" 

1633 # note: some locators return data limits, other return view limits, 

1634 # hence there is no *one* interface to call self.tick_values. 

1635 raise NotImplementedError('Derived must override') 

1636 

1637 def raise_if_exceeds(self, locs): 

1638 """ 

1639 Log at WARNING level if *locs* is longer than `Locator.MAXTICKS`. 

1640 

1641 This is intended to be called immediately before returning *locs* from 

1642 ``__call__`` to inform users in case their Locator returns a huge 

1643 number of ticks, causing Matplotlib to run out of memory. 

1644 

1645 The "strange" name of this method dates back to when it would raise an 

1646 exception instead of emitting a log. 

1647 """ 

1648 if len(locs) >= self.MAXTICKS: 

1649 _log.warning( 

1650 "Locator attempting to generate %s ticks ([%s, ..., %s]), " 

1651 "which exceeds Locator.MAXTICKS (%s).", 

1652 len(locs), locs[0], locs[-1], self.MAXTICKS) 

1653 return locs 

1654 

1655 def nonsingular(self, v0, v1): 

1656 """ 

1657 Adjust a range as needed to avoid singularities. 

1658 

1659 This method gets called during autoscaling, with ``(v0, v1)`` set to 

1660 the data limits on the Axes if the Axes contains any data, or 

1661 ``(-inf, +inf)`` if not. 

1662 

1663 - If ``v0 == v1`` (possibly up to some floating point slop), this 

1664 method returns an expanded interval around this value. 

1665 - If ``(v0, v1) == (-inf, +inf)``, this method returns appropriate 

1666 default view limits. 

1667 - Otherwise, ``(v0, v1)`` is returned without modification. 

1668 """ 

1669 return mtransforms.nonsingular(v0, v1, expander=.05) 

1670 

1671 def view_limits(self, vmin, vmax): 

1672 """ 

1673 Select a scale for the range from vmin to vmax. 

1674 

1675 Subclasses should override this method to change locator behaviour. 

1676 """ 

1677 return mtransforms.nonsingular(vmin, vmax) 

1678 

1679 

1680class IndexLocator(Locator): 

1681 """ 

1682 Place ticks at every nth point plotted. 

1683 

1684 IndexLocator assumes index plotting; i.e., that the ticks are placed at integer 

1685 values in the range between 0 and len(data) inclusive. 

1686 """ 

1687 def __init__(self, base, offset): 

1688 """Place ticks every *base* data point, starting at *offset*.""" 

1689 self._base = base 

1690 self.offset = offset 

1691 

1692 def set_params(self, base=None, offset=None): 

1693 """Set parameters within this locator""" 

1694 if base is not None: 

1695 self._base = base 

1696 if offset is not None: 

1697 self.offset = offset 

1698 

1699 def __call__(self): 

1700 """Return the locations of the ticks""" 

1701 dmin, dmax = self.axis.get_data_interval() 

1702 return self.tick_values(dmin, dmax) 

1703 

1704 def tick_values(self, vmin, vmax): 

1705 return self.raise_if_exceeds( 

1706 np.arange(vmin + self.offset, vmax + 1, self._base)) 

1707 

1708 

1709class FixedLocator(Locator): 

1710 r""" 

1711 Place ticks at a set of fixed values. 

1712 

1713 If *nbins* is None ticks are placed at all values. Otherwise, the *locs* array of 

1714 possible positions will be subsampled to keep the number of ticks 

1715 :math:`\leq nbins + 1`. The subsampling will be done to include the smallest 

1716 absolute value; for example, if zero is included in the array of possibilities, then 

1717 it will be included in the chosen ticks. 

1718 """ 

1719 

1720 def __init__(self, locs, nbins=None): 

1721 self.locs = np.asarray(locs) 

1722 _api.check_shape((None,), locs=self.locs) 

1723 self.nbins = max(nbins, 2) if nbins is not None else None 

1724 

1725 def set_params(self, nbins=None): 

1726 """Set parameters within this locator.""" 

1727 if nbins is not None: 

1728 self.nbins = nbins 

1729 

1730 def __call__(self): 

1731 return self.tick_values(None, None) 

1732 

1733 def tick_values(self, vmin, vmax): 

1734 """ 

1735 Return the locations of the ticks. 

1736 

1737 .. note:: 

1738 

1739 Because the values are fixed, vmin and vmax are not used in this 

1740 method. 

1741 

1742 """ 

1743 if self.nbins is None: 

1744 return self.locs 

1745 step = max(int(np.ceil(len(self.locs) / self.nbins)), 1) 

1746 ticks = self.locs[::step] 

1747 for i in range(1, step): 

1748 ticks1 = self.locs[i::step] 

1749 if np.abs(ticks1).min() < np.abs(ticks).min(): 

1750 ticks = ticks1 

1751 return self.raise_if_exceeds(ticks) 

1752 

1753 

1754class NullLocator(Locator): 

1755 """ 

1756 No ticks 

1757 """ 

1758 

1759 def __call__(self): 

1760 return self.tick_values(None, None) 

1761 

1762 def tick_values(self, vmin, vmax): 

1763 """ 

1764 Return the locations of the ticks. 

1765 

1766 .. note:: 

1767 

1768 Because the values are Null, vmin and vmax are not used in this 

1769 method. 

1770 """ 

1771 return [] 

1772 

1773 

1774class LinearLocator(Locator): 

1775 """ 

1776 Place ticks at evenly spaced values. 

1777 

1778 The first time this function is called it will try to set the 

1779 number of ticks to make a nice tick partitioning. Thereafter, the 

1780 number of ticks will be fixed so that interactive navigation will 

1781 be nice 

1782 

1783 """ 

1784 def __init__(self, numticks=None, presets=None): 

1785 """ 

1786 Parameters 

1787 ---------- 

1788 numticks : int or None, default None 

1789 Number of ticks. If None, *numticks* = 11. 

1790 presets : dict or None, default: None 

1791 Dictionary mapping ``(vmin, vmax)`` to an array of locations. 

1792 Overrides *numticks* if there is an entry for the current 

1793 ``(vmin, vmax)``. 

1794 """ 

1795 self.numticks = numticks 

1796 if presets is None: 

1797 self.presets = {} 

1798 else: 

1799 self.presets = presets 

1800 

1801 @property 

1802 def numticks(self): 

1803 # Old hard-coded default. 

1804 return self._numticks if self._numticks is not None else 11 

1805 

1806 @numticks.setter 

1807 def numticks(self, numticks): 

1808 self._numticks = numticks 

1809 

1810 def set_params(self, numticks=None, presets=None): 

1811 """Set parameters within this locator.""" 

1812 if presets is not None: 

1813 self.presets = presets 

1814 if numticks is not None: 

1815 self.numticks = numticks 

1816 

1817 def __call__(self): 

1818 """Return the locations of the ticks.""" 

1819 vmin, vmax = self.axis.get_view_interval() 

1820 return self.tick_values(vmin, vmax) 

1821 

1822 def tick_values(self, vmin, vmax): 

1823 vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) 

1824 

1825 if (vmin, vmax) in self.presets: 

1826 return self.presets[(vmin, vmax)] 

1827 

1828 if self.numticks == 0: 

1829 return [] 

1830 ticklocs = np.linspace(vmin, vmax, self.numticks) 

1831 

1832 return self.raise_if_exceeds(ticklocs) 

1833 

1834 def view_limits(self, vmin, vmax): 

1835 """Try to choose the view limits intelligently.""" 

1836 

1837 if vmax < vmin: 

1838 vmin, vmax = vmax, vmin 

1839 

1840 if vmin == vmax: 

1841 vmin -= 1 

1842 vmax += 1 

1843 

1844 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': 

1845 exponent, remainder = divmod( 

1846 math.log10(vmax - vmin), math.log10(max(self.numticks - 1, 1))) 

1847 exponent -= (remainder < .5) 

1848 scale = max(self.numticks - 1, 1) ** (-exponent) 

1849 vmin = math.floor(scale * vmin) / scale 

1850 vmax = math.ceil(scale * vmax) / scale 

1851 

1852 return mtransforms.nonsingular(vmin, vmax) 

1853 

1854 

1855class MultipleLocator(Locator): 

1856 """ 

1857 Place ticks at every integer multiple of a base plus an offset. 

1858 """ 

1859 

1860 def __init__(self, base=1.0, offset=0.0): 

1861 """ 

1862 Parameters 

1863 ---------- 

1864 base : float > 0, default: 1.0 

1865 Interval between ticks. 

1866 offset : float, default: 0.0 

1867 Value added to each multiple of *base*. 

1868 

1869 .. versionadded:: 3.8 

1870 """ 

1871 self._edge = _Edge_integer(base, 0) 

1872 self._offset = offset 

1873 

1874 def set_params(self, base=None, offset=None): 

1875 """ 

1876 Set parameters within this locator. 

1877 

1878 Parameters 

1879 ---------- 

1880 base : float > 0, optional 

1881 Interval between ticks. 

1882 offset : float, optional 

1883 Value added to each multiple of *base*. 

1884 

1885 .. versionadded:: 3.8 

1886 """ 

1887 if base is not None: 

1888 self._edge = _Edge_integer(base, 0) 

1889 if offset is not None: 

1890 self._offset = offset 

1891 

1892 def __call__(self): 

1893 """Return the locations of the ticks.""" 

1894 vmin, vmax = self.axis.get_view_interval() 

1895 return self.tick_values(vmin, vmax) 

1896 

1897 def tick_values(self, vmin, vmax): 

1898 if vmax < vmin: 

1899 vmin, vmax = vmax, vmin 

1900 step = self._edge.step 

1901 vmin -= self._offset 

1902 vmax -= self._offset 

1903 vmin = self._edge.ge(vmin) * step 

1904 n = (vmax - vmin + 0.001 * step) // step 

1905 locs = vmin - step + np.arange(n + 3) * step + self._offset 

1906 return self.raise_if_exceeds(locs) 

1907 

1908 def view_limits(self, dmin, dmax): 

1909 """ 

1910 Set the view limits to the nearest tick values that contain the data. 

1911 """ 

1912 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': 

1913 vmin = self._edge.le(dmin - self._offset) * self._edge.step + self._offset 

1914 vmax = self._edge.ge(dmax - self._offset) * self._edge.step + self._offset 

1915 if vmin == vmax: 

1916 vmin -= 1 

1917 vmax += 1 

1918 else: 

1919 vmin = dmin 

1920 vmax = dmax 

1921 

1922 return mtransforms.nonsingular(vmin, vmax) 

1923 

1924 

1925def scale_range(vmin, vmax, n=1, threshold=100): 

1926 dv = abs(vmax - vmin) # > 0 as nonsingular is called before. 

1927 meanv = (vmax + vmin) / 2 

1928 if abs(meanv) / dv < threshold: 

1929 offset = 0 

1930 else: 

1931 offset = math.copysign(10 ** (math.log10(abs(meanv)) // 1), meanv) 

1932 scale = 10 ** (math.log10(dv / n) // 1) 

1933 return scale, offset 

1934 

1935 

1936class _Edge_integer: 

1937 """ 

1938 Helper for `.MaxNLocator`, `.MultipleLocator`, etc. 

1939 

1940 Take floating-point precision limitations into account when calculating 

1941 tick locations as integer multiples of a step. 

1942 """ 

1943 def __init__(self, step, offset): 

1944 """ 

1945 Parameters 

1946 ---------- 

1947 step : float > 0 

1948 Interval between ticks. 

1949 offset : float 

1950 Offset subtracted from the data limits prior to calculating tick 

1951 locations. 

1952 """ 

1953 if step <= 0: 

1954 raise ValueError("'step' must be positive") 

1955 self.step = step 

1956 self._offset = abs(offset) 

1957 

1958 def closeto(self, ms, edge): 

1959 # Allow more slop when the offset is large compared to the step. 

1960 if self._offset > 0: 

1961 digits = np.log10(self._offset / self.step) 

1962 tol = max(1e-10, 10 ** (digits - 12)) 

1963 tol = min(0.4999, tol) 

1964 else: 

1965 tol = 1e-10 

1966 return abs(ms - edge) < tol 

1967 

1968 def le(self, x): 

1969 """Return the largest n: n*step <= x.""" 

1970 d, m = divmod(x, self.step) 

1971 if self.closeto(m / self.step, 1): 

1972 return d + 1 

1973 return d 

1974 

1975 def ge(self, x): 

1976 """Return the smallest n: n*step >= x.""" 

1977 d, m = divmod(x, self.step) 

1978 if self.closeto(m / self.step, 0): 

1979 return d 

1980 return d + 1 

1981 

1982 

1983class MaxNLocator(Locator): 

1984 """ 

1985 Place evenly spaced ticks, with a cap on the total number of ticks. 

1986 

1987 Finds nice tick locations with no more than :math:`nbins + 1` ticks being within the 

1988 view limits. Locations beyond the limits are added to support autoscaling. 

1989 """ 

1990 default_params = dict(nbins=10, 

1991 steps=None, 

1992 integer=False, 

1993 symmetric=False, 

1994 prune=None, 

1995 min_n_ticks=2) 

1996 

1997 def __init__(self, nbins=None, **kwargs): 

1998 """ 

1999 Parameters 

2000 ---------- 

2001 nbins : int or 'auto', default: 10 

2002 Maximum number of intervals; one less than max number of 

2003 ticks. If the string 'auto', the number of bins will be 

2004 automatically determined based on the length of the axis. 

2005 

2006 steps : array-like, optional 

2007 Sequence of acceptable tick multiples, starting with 1 and 

2008 ending with 10. For example, if ``steps=[1, 2, 4, 5, 10]``, 

2009 ``20, 40, 60`` or ``0.4, 0.6, 0.8`` would be possible 

2010 sets of ticks because they are multiples of 2. 

2011 ``30, 60, 90`` would not be generated because 3 does not 

2012 appear in this example list of steps. 

2013 

2014 integer : bool, default: False 

2015 If True, ticks will take only integer values, provided at least 

2016 *min_n_ticks* integers are found within the view limits. 

2017 

2018 symmetric : bool, default: False 

2019 If True, autoscaling will result in a range symmetric about zero. 

2020 

2021 prune : {'lower', 'upper', 'both', None}, default: None 

2022 Remove the 'lower' tick, the 'upper' tick, or ticks on 'both' sides 

2023 *if they fall exactly on an axis' edge* (this typically occurs when 

2024 :rc:`axes.autolimit_mode` is 'round_numbers'). Removing such ticks 

2025 is mostly useful for stacked or ganged plots, where the upper tick 

2026 of an Axes overlaps with the lower tick of the axes above it. 

2027 

2028 min_n_ticks : int, default: 2 

2029 Relax *nbins* and *integer* constraints if necessary to obtain 

2030 this minimum number of ticks. 

2031 """ 

2032 if nbins is not None: 

2033 kwargs['nbins'] = nbins 

2034 self.set_params(**{**self.default_params, **kwargs}) 

2035 

2036 @staticmethod 

2037 def _validate_steps(steps): 

2038 if not np.iterable(steps): 

2039 raise ValueError('steps argument must be an increasing sequence ' 

2040 'of numbers between 1 and 10 inclusive') 

2041 steps = np.asarray(steps) 

2042 if np.any(np.diff(steps) <= 0) or steps[-1] > 10 or steps[0] < 1: 

2043 raise ValueError('steps argument must be an increasing sequence ' 

2044 'of numbers between 1 and 10 inclusive') 

2045 if steps[0] != 1: 

2046 steps = np.concatenate([[1], steps]) 

2047 if steps[-1] != 10: 

2048 steps = np.concatenate([steps, [10]]) 

2049 return steps 

2050 

2051 @staticmethod 

2052 def _staircase(steps): 

2053 # Make an extended staircase within which the needed step will be 

2054 # found. This is probably much larger than necessary. 

2055 return np.concatenate([0.1 * steps[:-1], steps, [10 * steps[1]]]) 

2056 

2057 def set_params(self, **kwargs): 

2058 """ 

2059 Set parameters for this locator. 

2060 

2061 Parameters 

2062 ---------- 

2063 nbins : int or 'auto', optional 

2064 see `.MaxNLocator` 

2065 steps : array-like, optional 

2066 see `.MaxNLocator` 

2067 integer : bool, optional 

2068 see `.MaxNLocator` 

2069 symmetric : bool, optional 

2070 see `.MaxNLocator` 

2071 prune : {'lower', 'upper', 'both', None}, optional 

2072 see `.MaxNLocator` 

2073 min_n_ticks : int, optional 

2074 see `.MaxNLocator` 

2075 """ 

2076 if 'nbins' in kwargs: 

2077 self._nbins = kwargs.pop('nbins') 

2078 if self._nbins != 'auto': 

2079 self._nbins = int(self._nbins) 

2080 if 'symmetric' in kwargs: 

2081 self._symmetric = kwargs.pop('symmetric') 

2082 if 'prune' in kwargs: 

2083 prune = kwargs.pop('prune') 

2084 _api.check_in_list(['upper', 'lower', 'both', None], prune=prune) 

2085 self._prune = prune 

2086 if 'min_n_ticks' in kwargs: 

2087 self._min_n_ticks = max(1, kwargs.pop('min_n_ticks')) 

2088 if 'steps' in kwargs: 

2089 steps = kwargs.pop('steps') 

2090 if steps is None: 

2091 self._steps = np.array([1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10]) 

2092 else: 

2093 self._steps = self._validate_steps(steps) 

2094 self._extended_steps = self._staircase(self._steps) 

2095 if 'integer' in kwargs: 

2096 self._integer = kwargs.pop('integer') 

2097 if kwargs: 

2098 raise _api.kwarg_error("set_params", kwargs) 

2099 

2100 def _raw_ticks(self, vmin, vmax): 

2101 """ 

2102 Generate a list of tick locations including the range *vmin* to 

2103 *vmax*. In some applications, one or both of the end locations 

2104 will not be needed, in which case they are trimmed off 

2105 elsewhere. 

2106 """ 

2107 if self._nbins == 'auto': 

2108 if self.axis is not None: 

2109 nbins = np.clip(self.axis.get_tick_space(), 

2110 max(1, self._min_n_ticks - 1), 9) 

2111 else: 

2112 nbins = 9 

2113 else: 

2114 nbins = self._nbins 

2115 

2116 scale, offset = scale_range(vmin, vmax, nbins) 

2117 _vmin = vmin - offset 

2118 _vmax = vmax - offset 

2119 steps = self._extended_steps * scale 

2120 if self._integer: 

2121 # For steps > 1, keep only integer values. 

2122 igood = (steps < 1) | (np.abs(steps - np.round(steps)) < 0.001) 

2123 steps = steps[igood] 

2124 

2125 raw_step = ((_vmax - _vmin) / nbins) 

2126 if hasattr(self.axis, "axes") and self.axis.axes.name == '3d': 

2127 # Due to the change in automargin behavior in mpl3.9, we need to 

2128 # adjust the raw step to match the mpl3.8 appearance. The zoom 

2129 # factor of 2/48, gives us the 23/24 modifier. 

2130 raw_step = raw_step * 23/24 

2131 large_steps = steps >= raw_step 

2132 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': 

2133 # Classic round_numbers mode may require a larger step. 

2134 # Get first multiple of steps that are <= _vmin 

2135 floored_vmins = (_vmin // steps) * steps 

2136 floored_vmaxs = floored_vmins + steps * nbins 

2137 large_steps = large_steps & (floored_vmaxs >= _vmax) 

2138 

2139 # Find index of smallest large step 

2140 if any(large_steps): 

2141 istep = np.nonzero(large_steps)[0][0] 

2142 else: 

2143 istep = len(steps) - 1 

2144 

2145 # Start at smallest of the steps greater than the raw step, and check 

2146 # if it provides enough ticks. If not, work backwards through 

2147 # smaller steps until one is found that provides enough ticks. 

2148 for step in steps[:istep+1][::-1]: 

2149 

2150 if (self._integer and 

2151 np.floor(_vmax) - np.ceil(_vmin) >= self._min_n_ticks - 1): 

2152 step = max(1, step) 

2153 best_vmin = (_vmin // step) * step 

2154 

2155 # Find tick locations spanning the vmin-vmax range, taking into 

2156 # account degradation of precision when there is a large offset. 

2157 # The edge ticks beyond vmin and/or vmax are needed for the 

2158 # "round_numbers" autolimit mode. 

2159 edge = _Edge_integer(step, offset) 

2160 low = edge.le(_vmin - best_vmin) 

2161 high = edge.ge(_vmax - best_vmin) 

2162 ticks = np.arange(low, high + 1) * step + best_vmin 

2163 # Count only the ticks that will be displayed. 

2164 nticks = ((ticks <= _vmax) & (ticks >= _vmin)).sum() 

2165 if nticks >= self._min_n_ticks: 

2166 break 

2167 return ticks + offset 

2168 

2169 def __call__(self): 

2170 vmin, vmax = self.axis.get_view_interval() 

2171 return self.tick_values(vmin, vmax) 

2172 

2173 def tick_values(self, vmin, vmax): 

2174 if self._symmetric: 

2175 vmax = max(abs(vmin), abs(vmax)) 

2176 vmin = -vmax 

2177 vmin, vmax = mtransforms.nonsingular( 

2178 vmin, vmax, expander=1e-13, tiny=1e-14) 

2179 locs = self._raw_ticks(vmin, vmax) 

2180 

2181 prune = self._prune 

2182 if prune == 'lower': 

2183 locs = locs[1:] 

2184 elif prune == 'upper': 

2185 locs = locs[:-1] 

2186 elif prune == 'both': 

2187 locs = locs[1:-1] 

2188 return self.raise_if_exceeds(locs) 

2189 

2190 def view_limits(self, dmin, dmax): 

2191 if self._symmetric: 

2192 dmax = max(abs(dmin), abs(dmax)) 

2193 dmin = -dmax 

2194 

2195 dmin, dmax = mtransforms.nonsingular( 

2196 dmin, dmax, expander=1e-12, tiny=1e-13) 

2197 

2198 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': 

2199 return self._raw_ticks(dmin, dmax)[[0, -1]] 

2200 else: 

2201 return dmin, dmax 

2202 

2203 

2204def _is_decade(x, *, base=10, rtol=None): 

2205 """Return True if *x* is an integer power of *base*.""" 

2206 if not np.isfinite(x): 

2207 return False 

2208 if x == 0.0: 

2209 return True 

2210 lx = np.log(abs(x)) / np.log(base) 

2211 if rtol is None: 

2212 return np.isclose(lx, np.round(lx)) 

2213 else: 

2214 return np.isclose(lx, np.round(lx), rtol=rtol) 

2215 

2216 

2217def _decade_less_equal(x, base): 

2218 """ 

2219 Return the largest integer power of *base* that's less or equal to *x*. 

2220 

2221 If *x* is negative, the exponent will be *greater*. 

2222 """ 

2223 return (x if x == 0 else 

2224 -_decade_greater_equal(-x, base) if x < 0 else 

2225 base ** np.floor(np.log(x) / np.log(base))) 

2226 

2227 

2228def _decade_greater_equal(x, base): 

2229 """ 

2230 Return the smallest integer power of *base* that's greater or equal to *x*. 

2231 

2232 If *x* is negative, the exponent will be *smaller*. 

2233 """ 

2234 return (x if x == 0 else 

2235 -_decade_less_equal(-x, base) if x < 0 else 

2236 base ** np.ceil(np.log(x) / np.log(base))) 

2237 

2238 

2239def _decade_less(x, base): 

2240 """ 

2241 Return the largest integer power of *base* that's less than *x*. 

2242 

2243 If *x* is negative, the exponent will be *greater*. 

2244 """ 

2245 if x < 0: 

2246 return -_decade_greater(-x, base) 

2247 less = _decade_less_equal(x, base) 

2248 if less == x: 

2249 less /= base 

2250 return less 

2251 

2252 

2253def _decade_greater(x, base): 

2254 """ 

2255 Return the smallest integer power of *base* that's greater than *x*. 

2256 

2257 If *x* is negative, the exponent will be *smaller*. 

2258 """ 

2259 if x < 0: 

2260 return -_decade_less(-x, base) 

2261 greater = _decade_greater_equal(x, base) 

2262 if greater == x: 

2263 greater *= base 

2264 return greater 

2265 

2266 

2267def _is_close_to_int(x): 

2268 return math.isclose(x, round(x)) 

2269 

2270 

2271class LogLocator(Locator): 

2272 """ 

2273 Place logarithmically spaced ticks. 

2274 

2275 Places ticks at the values ``subs[j] * base**i``. 

2276 """ 

2277 

2278 @_api.delete_parameter("3.8", "numdecs") 

2279 def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): 

2280 """ 

2281 Parameters 

2282 ---------- 

2283 base : float, default: 10.0 

2284 The base of the log used, so major ticks are placed at ``base**n``, where 

2285 ``n`` is an integer. 

2286 subs : None or {'auto', 'all'} or sequence of float, default: (1.0,) 

2287 Gives the multiples of integer powers of the base at which to place ticks. 

2288 The default of ``(1.0, )`` places ticks only at integer powers of the base. 

2289 Permitted string values are ``'auto'`` and ``'all'``. Both of these use an 

2290 algorithm based on the axis view limits to determine whether and how to put 

2291 ticks between integer powers of the base: 

2292 - ``'auto'``: Ticks are placed only between integer powers. 

2293 - ``'all'``: Ticks are placed between *and* at integer powers. 

2294 - ``None``: Equivalent to ``'auto'``. 

2295 numticks : None or int, default: None 

2296 The maximum number of ticks to allow on a given axis. The default of 

2297 ``None`` will try to choose intelligently as long as this Locator has 

2298 already been assigned to an axis using `~.axis.Axis.get_tick_space`, but 

2299 otherwise falls back to 9. 

2300 """ 

2301 if numticks is None: 

2302 if mpl.rcParams['_internal.classic_mode']: 

2303 numticks = 15 

2304 else: 

2305 numticks = 'auto' 

2306 self._base = float(base) 

2307 self._set_subs(subs) 

2308 self._numdecs = numdecs 

2309 self.numticks = numticks 

2310 

2311 @_api.delete_parameter("3.8", "numdecs") 

2312 def set_params(self, base=None, subs=None, numdecs=None, numticks=None): 

2313 """Set parameters within this locator.""" 

2314 if base is not None: 

2315 self._base = float(base) 

2316 if subs is not None: 

2317 self._set_subs(subs) 

2318 if numdecs is not None: 

2319 self._numdecs = numdecs 

2320 if numticks is not None: 

2321 self.numticks = numticks 

2322 

2323 numdecs = _api.deprecate_privatize_attribute( 

2324 "3.8", addendum="This attribute has no effect.") 

2325 

2326 def _set_subs(self, subs): 

2327 """ 

2328 Set the minor ticks for the log scaling every ``base**i*subs[j]``. 

2329 """ 

2330 if subs is None: # consistency with previous bad API 

2331 self._subs = 'auto' 

2332 elif isinstance(subs, str): 

2333 _api.check_in_list(('all', 'auto'), subs=subs) 

2334 self._subs = subs 

2335 else: 

2336 try: 

2337 self._subs = np.asarray(subs, dtype=float) 

2338 except ValueError as e: 

2339 raise ValueError("subs must be None, 'all', 'auto' or " 

2340 "a sequence of floats, not " 

2341 f"{subs}.") from e 

2342 if self._subs.ndim != 1: 

2343 raise ValueError("A sequence passed to subs must be " 

2344 "1-dimensional, not " 

2345 f"{self._subs.ndim}-dimensional.") 

2346 

2347 def __call__(self): 

2348 """Return the locations of the ticks.""" 

2349 vmin, vmax = self.axis.get_view_interval() 

2350 return self.tick_values(vmin, vmax) 

2351 

2352 def tick_values(self, vmin, vmax): 

2353 if self.numticks == 'auto': 

2354 if self.axis is not None: 

2355 numticks = np.clip(self.axis.get_tick_space(), 2, 9) 

2356 else: 

2357 numticks = 9 

2358 else: 

2359 numticks = self.numticks 

2360 

2361 b = self._base 

2362 if vmin <= 0.0: 

2363 if self.axis is not None: 

2364 vmin = self.axis.get_minpos() 

2365 

2366 if vmin <= 0.0 or not np.isfinite(vmin): 

2367 raise ValueError( 

2368 "Data has no positive values, and therefore cannot be log-scaled.") 

2369 

2370 _log.debug('vmin %s vmax %s', vmin, vmax) 

2371 

2372 if vmax < vmin: 

2373 vmin, vmax = vmax, vmin 

2374 log_vmin = math.log(vmin) / math.log(b) 

2375 log_vmax = math.log(vmax) / math.log(b) 

2376 

2377 numdec = math.floor(log_vmax) - math.ceil(log_vmin) 

2378 

2379 if isinstance(self._subs, str): 

2380 if numdec > 10 or b < 3: 

2381 if self._subs == 'auto': 

2382 return np.array([]) # no minor or major ticks 

2383 else: 

2384 subs = np.array([1.0]) # major ticks 

2385 else: 

2386 _first = 2.0 if self._subs == 'auto' else 1.0 

2387 subs = np.arange(_first, b) 

2388 else: 

2389 subs = self._subs 

2390 

2391 # Get decades between major ticks. 

2392 stride = (max(math.ceil(numdec / (numticks - 1)), 1) 

2393 if mpl.rcParams['_internal.classic_mode'] else 

2394 numdec // numticks + 1) 

2395 

2396 # if we have decided that the stride is as big or bigger than 

2397 # the range, clip the stride back to the available range - 1 

2398 # with a floor of 1. This prevents getting axis with only 1 tick 

2399 # visible. 

2400 if stride >= numdec: 

2401 stride = max(1, numdec - 1) 

2402 

2403 # Does subs include anything other than 1? Essentially a hack to know 

2404 # whether we're a major or a minor locator. 

2405 have_subs = len(subs) > 1 or (len(subs) == 1 and subs[0] != 1.0) 

2406 

2407 decades = np.arange(math.floor(log_vmin) - stride, 

2408 math.ceil(log_vmax) + 2 * stride, stride) 

2409 

2410 if have_subs: 

2411 if stride == 1: 

2412 ticklocs = np.concatenate( 

2413 [subs * decade_start for decade_start in b ** decades]) 

2414 else: 

2415 ticklocs = np.array([]) 

2416 else: 

2417 ticklocs = b ** decades 

2418 

2419 _log.debug('ticklocs %r', ticklocs) 

2420 if (len(subs) > 1 

2421 and stride == 1 

2422 and ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() <= 1): 

2423 # If we're a minor locator *that expects at least two ticks per 

2424 # decade* and the major locator stride is 1 and there's no more 

2425 # than one minor tick, switch to AutoLocator. 

2426 return AutoLocator().tick_values(vmin, vmax) 

2427 else: 

2428 return self.raise_if_exceeds(ticklocs) 

2429 

2430 def view_limits(self, vmin, vmax): 

2431 """Try to choose the view limits intelligently.""" 

2432 b = self._base 

2433 

2434 vmin, vmax = self.nonsingular(vmin, vmax) 

2435 

2436 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': 

2437 vmin = _decade_less_equal(vmin, b) 

2438 vmax = _decade_greater_equal(vmax, b) 

2439 

2440 return vmin, vmax 

2441 

2442 def nonsingular(self, vmin, vmax): 

2443 if vmin > vmax: 

2444 vmin, vmax = vmax, vmin 

2445 if not np.isfinite(vmin) or not np.isfinite(vmax): 

2446 vmin, vmax = 1, 10 # Initial range, no data plotted yet. 

2447 elif vmax <= 0: 

2448 _api.warn_external( 

2449 "Data has no positive values, and therefore cannot be " 

2450 "log-scaled.") 

2451 vmin, vmax = 1, 10 

2452 else: 

2453 # Consider shared axises 

2454 minpos = min(axis.get_minpos() for axis in self.axis._get_shared_axis()) 

2455 if not np.isfinite(minpos): 

2456 minpos = 1e-300 # This should never take effect. 

2457 if vmin <= 0: 

2458 vmin = minpos 

2459 if vmin == vmax: 

2460 vmin = _decade_less(vmin, self._base) 

2461 vmax = _decade_greater(vmax, self._base) 

2462 return vmin, vmax 

2463 

2464 

2465class SymmetricalLogLocator(Locator): 

2466 """ 

2467 Place ticks spaced linearly near zero and spaced logarithmically beyond a threshold. 

2468 """ 

2469 

2470 def __init__(self, transform=None, subs=None, linthresh=None, base=None): 

2471 """ 

2472 Parameters 

2473 ---------- 

2474 transform : `~.scale.SymmetricalLogTransform`, optional 

2475 If set, defines the *base* and *linthresh* of the symlog transform. 

2476 base, linthresh : float, optional 

2477 The *base* and *linthresh* of the symlog transform, as documented 

2478 for `.SymmetricalLogScale`. These parameters are only used if 

2479 *transform* is not set. 

2480 subs : sequence of float, default: [1] 

2481 The multiples of integer powers of the base where ticks are placed, 

2482 i.e., ticks are placed at 

2483 ``[sub * base**i for i in ... for sub in subs]``. 

2484 

2485 Notes 

2486 ----- 

2487 Either *transform*, or both *base* and *linthresh*, must be given. 

2488 """ 

2489 if transform is not None: 

2490 self._base = transform.base 

2491 self._linthresh = transform.linthresh 

2492 elif linthresh is not None and base is not None: 

2493 self._base = base 

2494 self._linthresh = linthresh 

2495 else: 

2496 raise ValueError("Either transform, or both linthresh " 

2497 "and base, must be provided.") 

2498 if subs is None: 

2499 self._subs = [1.0] 

2500 else: 

2501 self._subs = subs 

2502 self.numticks = 15 

2503 

2504 def set_params(self, subs=None, numticks=None): 

2505 """Set parameters within this locator.""" 

2506 if numticks is not None: 

2507 self.numticks = numticks 

2508 if subs is not None: 

2509 self._subs = subs 

2510 

2511 def __call__(self): 

2512 """Return the locations of the ticks.""" 

2513 # Note, these are untransformed coordinates 

2514 vmin, vmax = self.axis.get_view_interval() 

2515 return self.tick_values(vmin, vmax) 

2516 

2517 def tick_values(self, vmin, vmax): 

2518 linthresh = self._linthresh 

2519 

2520 if vmax < vmin: 

2521 vmin, vmax = vmax, vmin 

2522 

2523 # The domain is divided into three sections, only some of 

2524 # which may actually be present. 

2525 # 

2526 # <======== -t ==0== t ========> 

2527 # aaaaaaaaa bbbbb ccccccccc 

2528 # 

2529 # a) and c) will have ticks at integral log positions. The 

2530 # number of ticks needs to be reduced if there are more 

2531 # than self.numticks of them. 

2532 # 

2533 # b) has a tick at 0 and only 0 (we assume t is a small 

2534 # number, and the linear segment is just an implementation 

2535 # detail and not interesting.) 

2536 # 

2537 # We could also add ticks at t, but that seems to usually be 

2538 # uninteresting. 

2539 # 

2540 # "simple" mode is when the range falls entirely within [-t, t] 

2541 # -- it should just display (vmin, 0, vmax) 

2542 if -linthresh <= vmin < vmax <= linthresh: 

2543 # only the linear range is present 

2544 return sorted({vmin, 0, vmax}) 

2545 

2546 # Lower log range is present 

2547 has_a = (vmin < -linthresh) 

2548 # Upper log range is present 

2549 has_c = (vmax > linthresh) 

2550 

2551 # Check if linear range is present 

2552 has_b = (has_a and vmax > -linthresh) or (has_c and vmin < linthresh) 

2553 

2554 base = self._base 

2555 

2556 def get_log_range(lo, hi): 

2557 lo = np.floor(np.log(lo) / np.log(base)) 

2558 hi = np.ceil(np.log(hi) / np.log(base)) 

2559 return lo, hi 

2560 

2561 # Calculate all the ranges, so we can determine striding 

2562 a_lo, a_hi = (0, 0) 

2563 if has_a: 

2564 a_upper_lim = min(-linthresh, vmax) 

2565 a_lo, a_hi = get_log_range(abs(a_upper_lim), abs(vmin) + 1) 

2566 

2567 c_lo, c_hi = (0, 0) 

2568 if has_c: 

2569 c_lower_lim = max(linthresh, vmin) 

2570 c_lo, c_hi = get_log_range(c_lower_lim, vmax + 1) 

2571 

2572 # Calculate the total number of integer exponents in a and c ranges 

2573 total_ticks = (a_hi - a_lo) + (c_hi - c_lo) 

2574 if has_b: 

2575 total_ticks += 1 

2576 stride = max(total_ticks // (self.numticks - 1), 1) 

2577 

2578 decades = [] 

2579 if has_a: 

2580 decades.extend(-1 * (base ** (np.arange(a_lo, a_hi, 

2581 stride)[::-1]))) 

2582 

2583 if has_b: 

2584 decades.append(0.0) 

2585 

2586 if has_c: 

2587 decades.extend(base ** (np.arange(c_lo, c_hi, stride))) 

2588 

2589 subs = np.asarray(self._subs) 

2590 

2591 if len(subs) > 1 or subs[0] != 1.0: 

2592 ticklocs = [] 

2593 for decade in decades: 

2594 if decade == 0: 

2595 ticklocs.append(decade) 

2596 else: 

2597 ticklocs.extend(subs * decade) 

2598 else: 

2599 ticklocs = decades 

2600 

2601 return self.raise_if_exceeds(np.array(ticklocs)) 

2602 

2603 def view_limits(self, vmin, vmax): 

2604 """Try to choose the view limits intelligently.""" 

2605 b = self._base 

2606 if vmax < vmin: 

2607 vmin, vmax = vmax, vmin 

2608 

2609 if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': 

2610 vmin = _decade_less_equal(vmin, b) 

2611 vmax = _decade_greater_equal(vmax, b) 

2612 if vmin == vmax: 

2613 vmin = _decade_less(vmin, b) 

2614 vmax = _decade_greater(vmax, b) 

2615 

2616 return mtransforms.nonsingular(vmin, vmax) 

2617 

2618 

2619class AsinhLocator(Locator): 

2620 """ 

2621 Place ticks spaced evenly on an inverse-sinh scale. 

2622 

2623 Generally used with the `~.scale.AsinhScale` class. 

2624 

2625 .. note:: 

2626 

2627 This API is provisional and may be revised in the future 

2628 based on early user feedback. 

2629 """ 

2630 def __init__(self, linear_width, numticks=11, symthresh=0.2, 

2631 base=10, subs=None): 

2632 """ 

2633 Parameters 

2634 ---------- 

2635 linear_width : float 

2636 The scale parameter defining the extent 

2637 of the quasi-linear region. 

2638 numticks : int, default: 11 

2639 The approximate number of major ticks that will fit 

2640 along the entire axis 

2641 symthresh : float, default: 0.2 

2642 The fractional threshold beneath which data which covers 

2643 a range that is approximately symmetric about zero 

2644 will have ticks that are exactly symmetric. 

2645 base : int, default: 10 

2646 The number base used for rounding tick locations 

2647 on a logarithmic scale. If this is less than one, 

2648 then rounding is to the nearest integer multiple 

2649 of powers of ten. 

2650 subs : tuple, default: None 

2651 Multiples of the number base, typically used 

2652 for the minor ticks, e.g. (2, 5) when base=10. 

2653 """ 

2654 super().__init__() 

2655 self.linear_width = linear_width 

2656 self.numticks = numticks 

2657 self.symthresh = symthresh 

2658 self.base = base 

2659 self.subs = subs 

2660 

2661 def set_params(self, numticks=None, symthresh=None, 

2662 base=None, subs=None): 

2663 """Set parameters within this locator.""" 

2664 if numticks is not None: 

2665 self.numticks = numticks 

2666 if symthresh is not None: 

2667 self.symthresh = symthresh 

2668 if base is not None: 

2669 self.base = base 

2670 if subs is not None: 

2671 self.subs = subs if len(subs) > 0 else None 

2672 

2673 def __call__(self): 

2674 vmin, vmax = self.axis.get_view_interval() 

2675 if (vmin * vmax) < 0 and abs(1 + vmax / vmin) < self.symthresh: 

2676 # Data-range appears to be almost symmetric, so round up: 

2677 bound = max(abs(vmin), abs(vmax)) 

2678 return self.tick_values(-bound, bound) 

2679 else: 

2680 return self.tick_values(vmin, vmax) 

2681 

2682 def tick_values(self, vmin, vmax): 

2683 # Construct a set of uniformly-spaced "on-screen" locations. 

2684 ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) 

2685 / self.linear_width) 

2686 ys = np.linspace(ymin, ymax, self.numticks) 

2687 zero_dev = abs(ys / (ymax - ymin)) 

2688 if ymin * ymax < 0: 

2689 # Ensure that the zero tick-mark is included, if the axis straddles zero. 

2690 ys = np.hstack([ys[(zero_dev > 0.5 / self.numticks)], 0.0]) 

2691 

2692 # Transform the "on-screen" grid to the data space: 

2693 xs = self.linear_width * np.sinh(ys / self.linear_width) 

2694 zero_xs = (ys == 0) 

2695 

2696 # Round the data-space values to be intuitive base-n numbers, keeping track of 

2697 # positive and negative values separately and carefully treating the zero value. 

2698 with np.errstate(divide="ignore"): # base ** log(0) = base ** -inf = 0. 

2699 if self.base > 1: 

2700 pows = (np.sign(xs) 

2701 * self.base ** np.floor(np.log(abs(xs)) / math.log(self.base))) 

2702 qs = np.outer(pows, self.subs).flatten() if self.subs else pows 

2703 else: # No need to adjust sign(pows), as it cancels out when computing qs. 

2704 pows = np.where(zero_xs, 1, 10**np.floor(np.log10(abs(xs)))) 

2705 qs = pows * np.round(xs / pows) 

2706 ticks = np.array(sorted(set(qs))) 

2707 

2708 return ticks if len(ticks) >= 2 else np.linspace(vmin, vmax, self.numticks) 

2709 

2710 

2711class LogitLocator(MaxNLocator): 

2712 """ 

2713 Place ticks spaced evenly on a logit scale. 

2714 """ 

2715 

2716 def __init__(self, minor=False, *, nbins="auto"): 

2717 """ 

2718 Parameters 

2719 ---------- 

2720 nbins : int or 'auto', optional 

2721 Number of ticks. Only used if minor is False. 

2722 minor : bool, default: False 

2723 Indicate if this locator is for minor ticks or not. 

2724 """ 

2725 

2726 self._minor = minor 

2727 super().__init__(nbins=nbins, steps=[1, 2, 5, 10]) 

2728 

2729 def set_params(self, minor=None, **kwargs): 

2730 """Set parameters within this locator.""" 

2731 if minor is not None: 

2732 self._minor = minor 

2733 super().set_params(**kwargs) 

2734 

2735 @property 

2736 def minor(self): 

2737 return self._minor 

2738 

2739 @minor.setter 

2740 def minor(self, value): 

2741 self.set_params(minor=value) 

2742 

2743 def tick_values(self, vmin, vmax): 

2744 # dummy axis has no axes attribute 

2745 if hasattr(self.axis, "axes") and self.axis.axes.name == "polar": 

2746 raise NotImplementedError("Polar axis cannot be logit scaled yet") 

2747 

2748 if self._nbins == "auto": 

2749 if self.axis is not None: 

2750 nbins = self.axis.get_tick_space() 

2751 if nbins < 2: 

2752 nbins = 2 

2753 else: 

2754 nbins = 9 

2755 else: 

2756 nbins = self._nbins 

2757 

2758 # We define ideal ticks with their index: 

2759 # linscale: ... 1e-3 1e-2 1e-1 1/2 1-1e-1 1-1e-2 1-1e-3 ... 

2760 # b-scale : ... -3 -2 -1 0 1 2 3 ... 

2761 def ideal_ticks(x): 

2762 return 10 ** x if x < 0 else 1 - (10 ** (-x)) if x > 0 else 0.5 

2763 

2764 vmin, vmax = self.nonsingular(vmin, vmax) 

2765 binf = int( 

2766 np.floor(np.log10(vmin)) 

2767 if vmin < 0.5 

2768 else 0 

2769 if vmin < 0.9 

2770 else -np.ceil(np.log10(1 - vmin)) 

2771 ) 

2772 bsup = int( 

2773 np.ceil(np.log10(vmax)) 

2774 if vmax <= 0.5 

2775 else 1 

2776 if vmax <= 0.9 

2777 else -np.floor(np.log10(1 - vmax)) 

2778 ) 

2779 numideal = bsup - binf - 1 

2780 if numideal >= 2: 

2781 # have 2 or more wanted ideal ticks, so use them as major ticks 

2782 if numideal > nbins: 

2783 # to many ideal ticks, subsampling ideals for major ticks, and 

2784 # take others for minor ticks 

2785 subsampling_factor = math.ceil(numideal / nbins) 

2786 if self._minor: 

2787 ticklocs = [ 

2788 ideal_ticks(b) 

2789 for b in range(binf, bsup + 1) 

2790 if (b % subsampling_factor) != 0 

2791 ] 

2792 else: 

2793 ticklocs = [ 

2794 ideal_ticks(b) 

2795 for b in range(binf, bsup + 1) 

2796 if (b % subsampling_factor) == 0 

2797 ] 

2798 return self.raise_if_exceeds(np.array(ticklocs)) 

2799 if self._minor: 

2800 ticklocs = [] 

2801 for b in range(binf, bsup): 

2802 if b < -1: 

2803 ticklocs.extend(np.arange(2, 10) * 10 ** b) 

2804 elif b == -1: 

2805 ticklocs.extend(np.arange(2, 5) / 10) 

2806 elif b == 0: 

2807 ticklocs.extend(np.arange(6, 9) / 10) 

2808 else: 

2809 ticklocs.extend( 

2810 1 - np.arange(2, 10)[::-1] * 10 ** (-b - 1) 

2811 ) 

2812 return self.raise_if_exceeds(np.array(ticklocs)) 

2813 ticklocs = [ideal_ticks(b) for b in range(binf, bsup + 1)] 

2814 return self.raise_if_exceeds(np.array(ticklocs)) 

2815 # the scale is zoomed so same ticks as linear scale can be used 

2816 if self._minor: 

2817 return [] 

2818 return super().tick_values(vmin, vmax) 

2819 

2820 def nonsingular(self, vmin, vmax): 

2821 standard_minpos = 1e-7 

2822 initial_range = (standard_minpos, 1 - standard_minpos) 

2823 if vmin > vmax: 

2824 vmin, vmax = vmax, vmin 

2825 if not np.isfinite(vmin) or not np.isfinite(vmax): 

2826 vmin, vmax = initial_range # Initial range, no data plotted yet. 

2827 elif vmax <= 0 or vmin >= 1: 

2828 # vmax <= 0 occurs when all values are negative 

2829 # vmin >= 1 occurs when all values are greater than one 

2830 _api.warn_external( 

2831 "Data has no values between 0 and 1, and therefore cannot be " 

2832 "logit-scaled." 

2833 ) 

2834 vmin, vmax = initial_range 

2835 else: 

2836 minpos = ( 

2837 self.axis.get_minpos() 

2838 if self.axis is not None 

2839 else standard_minpos 

2840 ) 

2841 if not np.isfinite(minpos): 

2842 minpos = standard_minpos # This should never take effect. 

2843 if vmin <= 0: 

2844 vmin = minpos 

2845 # NOTE: for vmax, we should query a property similar to get_minpos, 

2846 # but related to the maximal, less-than-one data point. 

2847 # Unfortunately, Bbox._minpos is defined very deep in the BBox and 

2848 # updated with data, so for now we use 1 - minpos as a substitute. 

2849 if vmax >= 1: 

2850 vmax = 1 - minpos 

2851 if vmin == vmax: 

2852 vmin, vmax = 0.1 * vmin, 1 - 0.1 * vmin 

2853 

2854 return vmin, vmax 

2855 

2856 

2857class AutoLocator(MaxNLocator): 

2858 """ 

2859 Place evenly spaced ticks, with the step size and maximum number of ticks chosen 

2860 automatically. 

2861 

2862 This is a subclass of `~matplotlib.ticker.MaxNLocator`, with parameters 

2863 *nbins = 'auto'* and *steps = [1, 2, 2.5, 5, 10]*. 

2864 """ 

2865 def __init__(self): 

2866 """ 

2867 To know the values of the non-public parameters, please have a 

2868 look to the defaults of `~matplotlib.ticker.MaxNLocator`. 

2869 """ 

2870 if mpl.rcParams['_internal.classic_mode']: 

2871 nbins = 9 

2872 steps = [1, 2, 5, 10] 

2873 else: 

2874 nbins = 'auto' 

2875 steps = [1, 2, 2.5, 5, 10] 

2876 super().__init__(nbins=nbins, steps=steps) 

2877 

2878 

2879class AutoMinorLocator(Locator): 

2880 """ 

2881 Place evenly spaced minor ticks, with the step size and maximum number of ticks 

2882 chosen automatically. 

2883 

2884 The Axis scale must be linear with evenly spaced major ticks . 

2885 """ 

2886 

2887 def __init__(self, n=None): 

2888 """ 

2889 *n* is the number of subdivisions of the interval between 

2890 major ticks; e.g., n=2 will place a single minor tick midway 

2891 between major ticks. 

2892 

2893 If *n* is omitted or None, the value stored in rcParams will be used. 

2894 In case *n* is set to 'auto', it will be set to 4 or 5. If the distance 

2895 between the major ticks equals 1, 2.5, 5 or 10 it can be perfectly 

2896 divided in 5 equidistant sub-intervals with a length multiple of 

2897 0.05. Otherwise it is divided in 4 sub-intervals. 

2898 """ 

2899 self.ndivs = n 

2900 

2901 def __call__(self): 

2902 # docstring inherited 

2903 if self.axis.get_scale() == 'log': 

2904 _api.warn_external('AutoMinorLocator does not work on logarithmic scales') 

2905 return [] 

2906 

2907 majorlocs = np.unique(self.axis.get_majorticklocs()) 

2908 if len(majorlocs) < 2: 

2909 # Need at least two major ticks to find minor tick locations. 

2910 # TODO: Figure out a way to still be able to display minor ticks with less 

2911 # than two major ticks visible. For now, just display no ticks at all. 

2912 return [] 

2913 majorstep = majorlocs[1] - majorlocs[0] 

2914 

2915 if self.ndivs is None: 

2916 self.ndivs = mpl.rcParams[ 

2917 'ytick.minor.ndivs' if self.axis.axis_name == 'y' 

2918 else 'xtick.minor.ndivs'] # for x and z axis 

2919 

2920 if self.ndivs == 'auto': 

2921 majorstep_mantissa = 10 ** (np.log10(majorstep) % 1) 

2922 ndivs = 5 if np.isclose(majorstep_mantissa, [1, 2.5, 5, 10]).any() else 4 

2923 else: 

2924 ndivs = self.ndivs 

2925 

2926 minorstep = majorstep / ndivs 

2927 

2928 vmin, vmax = sorted(self.axis.get_view_interval()) 

2929 t0 = majorlocs[0] 

2930 tmin = round((vmin - t0) / minorstep) 

2931 tmax = round((vmax - t0) / minorstep) + 1 

2932 locs = (np.arange(tmin, tmax) * minorstep) + t0 

2933 

2934 return self.raise_if_exceeds(locs) 

2935 

2936 def tick_values(self, vmin, vmax): 

2937 raise NotImplementedError( 

2938 f"Cannot get tick locations for a {type(self).__name__}")