| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 218a42b commit 61833b8
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,13 @@ | |||
| 1 | + ``matplotlib.ticker.EngFormatter`` can computes offsets now | ||
| 2 | + ----------------------------------------------------------- | ||
| 3 | + | ||
| 4 | + `matplotlib.ticker.EngFormatter` has gained the ability to show an offset text near the | ||
| 5 | + axis. Using logic shared with `matplotlib.ticker.ScalarFormatter`, it is capable of | ||
| 6 | + deciding whether the data qualifies having an offset and show it with an appropriate SI | ||
| 7 | + quantity prefix, and with the supplied ``unit``. | ||
| 8 | + | ||
| 9 | + To enable this new behavior, simply pass ``useOffset=True`` when you | ||
| 10 | + instantiate `matplotlib.ticker.EngFormatter`. See example | ||
| 11 | + :doc:`/gallery/ticks/engformatter_offset`. | ||
| 12 | + | ||
| 13 | + .. plot:: gallery/ticks/engformatter_offset.py | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,4 @@ | |||
| 1 | + Miscellaneous Changes | ||
| 2 | + --------------------- | ||
| 3 | + | ||
| 4 | + - The `matplotlib.ticker.ScalarFormatter` class has gained a new instantiating parameter ``usetex``. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,33 @@ | |||
| 1 | + """ | ||
| 2 | + =================================================== | ||
| 3 | + SI prefixed offsets and natural order of magnitudes | ||
| 4 | + =================================================== | ||
| 5 | + | ||
| 6 | + `matplotlib.ticker.EngFormatter` is capable of computing a natural | ||
| 7 | + offset for your axis data, and presenting it with a standard SI prefix | ||
| 8 | + automatically calculated. | ||
| 9 | + | ||
| 10 | + Below is an examples of such a plot: | ||
| 11 | + | ||
| 12 | + """ | ||
| 13 | + | ||
| 14 | + import matplotlib.pyplot as plt | ||
| 15 | + import numpy as np | ||
| 16 | + | ||
| 17 | + import matplotlib.ticker as mticker | ||
| 18 | + | ||
| 19 | + # Fixing random state for reproducibility | ||
| 20 | + np.random.seed(19680801) | ||
| 21 | + | ||
| 22 | + UNIT = "Hz" | ||
| 23 | + | ||
| 24 | + fig, ax = plt.subplots() | ||
| 25 | + ax.yaxis.set_major_formatter(mticker.EngFormatter( | ||
| 26 | + useOffset=True, | ||
| 27 | + unit=UNIT | ||
| 28 | + )) | ||
| 29 | + size = 100 | ||
| 30 | + measurement = np.full(size, 1e9) | ||
| 31 | + noise = np.random.uniform(low=-2e3, high=2e3, size=size) | ||
| 32 | + ax.plot(measurement + noise) | ||
| 33 | + plt.show() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1591,6 +1591,73 @@ def test_engformatter_usetex_useMathText(): | |||
| 1591 | 1591 | assert x_tick_label_text == ['$0$', '$500$', '$1$ k'] | |
| 1592 | 1592 | ||
| 1593 | 1593 | ||
| 1594 | + @pytest.mark.parametrize( | ||
| 1595 | + 'data_offset, noise, oom_center_desired, oom_noise_desired', [ | ||
| 1596 | + (271_490_000_000.0, 10, 9, 0), | ||
| 1597 | + (27_149_000_000_000.0, 10_000_000, 12, 6), | ||
| 1598 | + (27.149, 0.01, 0, -3), | ||
| 1599 | + (2_714.9, 0.01, 3, -3), | ||
| 1600 | + (271_490.0, 0.001, 3, -3), | ||
| 1601 | + (271.49, 0.001, 0, -3), | ||
| 1602 | + # The following sets of parameters demonstrates that when | ||
| 1603 | + # oom(data_offset)-1 and oom(noise)-2 equal a standard 3*N oom, we get | ||
| 1604 | + # that oom_noise_desired < oom(noise) | ||
| 1605 | + (27_149_000_000.0, 100, 9, +3), | ||
| 1606 | + (27.149, 1e-07, 0, -6), | ||
| 1607 | + (271.49, 0.0001, 0, -3), | ||
| 1608 | + (27.149, 0.0001, 0, -3), | ||
| 1609 | + # Tests where oom(data_offset) <= oom(noise), those are probably | ||
| 1610 | + # covered by the part where formatter.offset != 0 | ||
| 1611 | + (27_149.0, 10_000, 0, 3), | ||
| 1612 | + (27.149, 10_000, 0, 3), | ||
| 1613 | + (27.149, 1_000, 0, 3), | ||
| 1614 | + (27.149, 100, 0, 0), | ||
| 1615 | + (27.149, 10, 0, 0), | ||
| 1616 | + ] | ||
| 1617 | + ) | ||
| 1618 | + def test_engformatter_offset_oom( | ||
| 1619 | + data_offset, | ||
| 1620 | + noise, | ||
| 1621 | + oom_center_desired, | ||
| 1622 | + oom_noise_desired | ||
| 1623 | + ): | ||
| 1624 | + UNIT = "eV" | ||
| 1625 | + fig, ax = plt.subplots() | ||
| 1626 | + ydata = data_offset + np.arange(-5, 7, dtype=float)*noise | ||
| 1627 | + ax.plot(ydata) | ||
| 1628 | + formatter = mticker.EngFormatter(useOffset=True, unit=UNIT) | ||
| 1629 | + # So that offset strings will always have the same size | ||
| 1630 | + formatter.ENG_PREFIXES[0] = "_" | ||
| 1631 | + ax.yaxis.set_major_formatter(formatter) | ||
| 1632 | + fig.canvas.draw() | ||
| 1633 | + offset_got = formatter.get_offset() | ||
| 1634 | + ticks_got = [labl.get_text() for labl in ax.get_yticklabels()] | ||
| 1635 | + # Predicting whether offset should be 0 or not is essentially testing | ||
| 1636 | + # ScalarFormatter._compute_offset . This function is pretty complex and it | ||
| 1637 | + # would be nice to test it, but this is out of scope for this test which | ||
| 1638 | + # only makes sure that offset text and the ticks gets the correct unit | ||
| 1639 | + # prefixes and the ticks. | ||
| 1640 | + if formatter.offset: | ||
| 1641 | + prefix_noise_got = offset_got[2] | ||
| 1642 | + prefix_noise_desired = formatter.ENG_PREFIXES[oom_noise_desired] | ||
| 1643 | + prefix_center_got = offset_got[-1-len(UNIT)] | ||
| 1644 | + prefix_center_desired = formatter.ENG_PREFIXES[oom_center_desired] | ||
| 1645 | + assert prefix_noise_desired == prefix_noise_got | ||
| 1646 | + assert prefix_center_desired == prefix_center_got | ||
| 1647 | + # Make sure the ticks didn't get the UNIT | ||
| 1648 | + for tick in ticks_got: | ||
| 1649 | + assert UNIT not in tick | ||
| 1650 | + else: | ||
| 1651 | + assert oom_center_desired == 0 | ||
| 1652 | + assert offset_got == "" | ||
| 1653 | + # Make sure the ticks contain now the prefixes | ||
| 1654 | + for tick in ticks_got: | ||
| 1655 | + # 0 is zero on all orders of magnitudes, no matter what is | ||
| 1656 | + # oom_noise_desired | ||
| 1657 | + prefix_idx = 0 if tick[0] == "0" else oom_noise_desired | ||
| 1658 | + assert tick.endswith(formatter.ENG_PREFIXES[prefix_idx] + UNIT) | ||
| 1659 | + | ||
| 1660 | + | ||
| 1594 | 1661 | class TestPercentFormatter: | |
| 1595 | 1662 | percent_data = [ | |
| 1596 | 1663 | # Check explicitly set decimals over different intervals and values | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -407,6 +407,11 @@ class ScalarFormatter(Formatter): | |||
| 407 | 407 | useLocale : bool, default: :rc:`axes.formatter.use_locale`. | |
| 408 | 408 | Whether to use locale settings for decimal sign and positive sign. | |
| 409 | 409 | See `.set_useLocale`. | |
| 410 | + usetex : bool, default: :rc:`text.usetex` | ||
| 411 | + To enable/disable the use of TeX's math mode for rendering the | ||
| 412 | + numbers in the formatter. | ||
| 413 | + | ||
| 414 | + .. versionadded:: 3.10 | ||
| 410 | 415 | ||
| 411 | 416 | Notes | |
| 412 | 417 | ----- | |
@@ -444,20 +449,29 @@ class ScalarFormatter(Formatter): | |||
| 444 | 449 | ||
| 445 | 450 | """ | |
| 446 | 451 | ||
| 447 | - def __init__(self, useOffset=None, useMathText=None, useLocale=None): | ||
| 452 | + def __init__(self, useOffset=None, useMathText=None, useLocale=None, *, | ||
| 453 | + usetex=None): | ||
| 448 | 454 | if useOffset is None: | |
| 449 | 455 | useOffset = mpl.rcParams['axes.formatter.useoffset'] | |
| 450 | 456 | self._offset_threshold = \ | |
| 451 | 457 | mpl.rcParams['axes.formatter.offset_threshold'] | |
| 452 | 458 | self.set_useOffset(useOffset) | |
| 453 | - self._usetex = mpl.rcParams['text.usetex'] | ||
| 459 | + self.set_usetex(usetex) | ||
| 454 | 460 | self.set_useMathText(useMathText) | |
| 455 | 461 | self.orderOfMagnitude = 0 | |
| 456 | 462 | self.format = '' | |
| 457 | 463 | self._scientific = True | |
| 458 | 464 | self._powerlimits = mpl.rcParams['axes.formatter.limits'] | |
| 459 | 465 | self.set_useLocale(useLocale) | |
| 460 | 466 | ||
| 467 | + def get_usetex(self): | ||
| 468 | + return self._usetex | ||
| 469 | + | ||
| 470 | + def set_usetex(self, val): | ||
| 471 | + self._usetex = mpl._val_or_rc(val, 'text.usetex') | ||
| 472 | + | ||
| 473 | + usetex = property(fget=get_usetex, fset=set_usetex) | ||
| 474 | + | ||
| 461 | 475 | def get_useOffset(self): | |
| 462 | 476 | """ | |
| 463 | 477 | Return whether automatic mode for offset notation is active. | |
@@ -1324,7 +1338,7 @@ def format_data_short(self, value): | |||
| 1324 | 1338 | return f"1-{1 - value:e}" | |
| 1325 | 1339 | ||
| 1326 | 1340 | ||
| 1327 | - class EngFormatter(Formatter): | ||
| 1341 | + class EngFormatter(ScalarFormatter): | ||
| 1328 | 1342 | """ | |
| 1329 | 1343 | Format axis values using engineering prefixes to represent powers | |
| 1330 | 1344 | of 1000, plus a specified unit, e.g., 10 MHz instead of 1e7. | |
@@ -1356,7 +1370,7 @@ class EngFormatter(Formatter): | |||
| 1356 | 1370 | } | |
| 1357 | 1371 | ||
| 1358 | 1372 | def __init__(self, unit="", places=None, sep=" ", *, usetex=None, | |
| 1359 | - useMathText=None): | ||
| 1373 | + useMathText=None, useOffset=False): | ||
| 1360 | 1374 | r""" | |
| 1361 | 1375 | Parameters | |
| 1362 | 1376 | ---------- | |
@@ -1390,76 +1404,124 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, | |||
| 1390 | 1404 | useMathText : bool, default: :rc:`axes.formatter.use_mathtext` | |
| 1391 | 1405 | To enable/disable the use mathtext for rendering the numbers in | |
| 1392 | 1406 | the formatter. | |
| 1407 | + useOffset : bool or float, default: False | ||
| 1408 | + Whether to use offset notation with :math:`10^{3*N}` based prefixes. | ||
| 1409 | + This features allows showing an offset with standard SI order of | ||
| 1410 | + magnitude prefix near the axis. Offset is computed similarly to | ||
| 1411 | + how `ScalarFormatter` computes it internally, but here you are | ||
| 1412 | + guaranteed to get an offset which will make the tick labels exceed | ||
| 1413 | + 3 digits. See also `.set_useOffset`. | ||
| 1414 | + | ||
| 1415 | + .. versionadded:: 3.10 | ||
| 1393 | 1416 | """ | |
| 1394 | 1417 | self.unit = unit | |
| 1395 | 1418 | self.places = places | |
| 1396 | 1419 | 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) | ||
| 1420 | + super().__init__( | ||
| 1421 | + useOffset=useOffset, | ||
| 1422 | + useMathText=useMathText, | ||
| 1423 | + useLocale=False, | ||
| 1424 | + usetex=usetex, | ||
| 1425 | + ) | ||
| 1410 | 1426 | ||
| 1411 | - def get_useMathText(self): | ||
| 1412 | - return self._useMathText | ||
| 1427 | + def __call__(self, x, pos=None): | ||
| 1428 | + """ | ||
| 1429 | + Return the format for tick value *x* at position *pos*. | ||
| 1413 | 1430 | ||
| 1414 | - def set_useMathText(self, val): | ||
| 1415 | - if val is None: | ||
| 1416 | - self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] | ||
| 1431 | + If there is no currently offset in the data, it returns the best | ||
| 1432 | + engineering formatting that fits the given argument, independently. | ||
| 1433 | + """ | ||
| 1434 | + if len(self.locs) == 0 or self.offset == 0: | ||
| 1435 | + return self.fix_minus(self.format_data(x)) | ||
| 1417 | 1436 | else: | |
| 1418 | - self._useMathText = val | ||
| 1437 | + xp = (x - self.offset) / (10. ** self.orderOfMagnitude) | ||
| 1438 | + if abs(xp) < 1e-8: | ||
| 1439 | + xp = 0 | ||
| 1440 | + return self._format_maybe_minus_and_locale(self.format, xp) | ||
| 1419 | 1441 | ||
| 1420 | - useMathText = property(fget=get_useMathText, fset=set_useMathText) | ||
| 1442 | + def set_locs(self, locs): | ||
| 1443 | + # docstring inherited | ||
| 1444 | + self.locs = locs | ||
| 1445 | + if len(self.locs) > 0: | ||
| 1446 | + vmin, vmax = sorted(self.axis.get_view_interval()) | ||
| 1447 | + if self._useOffset: | ||
| 1448 | + self._compute_offset() | ||
| 1449 | + if self.offset != 0: | ||
| 1450 | + # We don't want to use the offset computed by | ||
| 1451 | + # self._compute_offset because it rounds the offset unaware | ||
| 1452 | + # of our engineering prefixes preference, and this can | ||
| 1453 | + # cause ticks with 4+ digits to appear. These ticks are | ||
| 1454 | + # slightly less readable, so if offset is justified | ||
| 1455 | + # (decided by self._compute_offset) we set it to better | ||
| 1456 | + # value: | ||
| 1457 | + self.offset = round((vmin + vmax)/2, 3) | ||
| 1458 | + # Use log1000 to use engineers' oom standards | ||
| 1459 | + self.orderOfMagnitude = math.floor(math.log(vmax - vmin, 1000))*3 | ||
| 1460 | + self._set_format() | ||
| 1421 | 1461 | ||
| 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) | ||
| 1462 | + # Simplify a bit ScalarFormatter.get_offset: We always want to use | ||
| 1463 | + # self.format_data. Also we want to return a non-empty string only if there | ||
| 1464 | + # is an offset, no matter what is self.orderOfMagnitude. If there _is_ an | ||
| 1465 | + # offset, self.orderOfMagnitude is consulted. This behavior is verified | ||
| 1466 | + # in `test_ticker.py`. | ||
| 1467 | + def get_offset(self): | ||
| 1468 | + # docstring inherited | ||
| 1469 | + if len(self.locs) == 0: | ||
| 1470 | + return '' | ||
| 1471 | + if self.offset: | ||
| 1472 | + offsetStr = '' | ||
| 1473 | + if self.offset: | ||
| 1474 | + offsetStr = self.format_data(self.offset) | ||
| 1475 | + if self.offset > 0: | ||
| 1476 | + offsetStr = '+' + offsetStr | ||
| 1477 | + sciNotStr = self.format_data(10 ** self.orderOfMagnitude) | ||
| 1478 | + if self._useMathText or self._usetex: | ||
| 1479 | + if sciNotStr != '': | ||
| 1480 | + sciNotStr = r'\times%s' % sciNotStr | ||
| 1481 | + s = f'${sciNotStr}{offsetStr}$' | ||
| 1482 | + else: | ||
| 1483 | + s = sciNotStr + offsetStr | ||
| 1484 | + return self.fix_minus(s) | ||
| 1485 | + return '' | ||
| 1428 | 1486 | ||
| 1429 | 1487 | def format_eng(self, num): | |
| 1488 | + """Alias to EngFormatter.format_data""" | ||
| 1489 | + return self.format_data(num) | ||
| 1490 | + | ||
| 1491 | + def format_data(self, value): | ||
| 1430 | 1492 | """ | |
| 1431 | 1493 | Format a number in engineering notation, appending a letter | |
| 1432 | 1494 | representing the power of 1000 of the original number. | |
| 1433 | 1495 | Some examples: | |
| 1434 | 1496 | ||
| 1435 | - >>> format_eng(0) # for self.places = 0 | ||
| 1497 | + >>> format_data(0) # for self.places = 0 | ||
| 1436 | 1498 | '0' | |
| 1437 | 1499 | ||
| 1438 | - >>> format_eng(1000000) # for self.places = 1 | ||
| 1500 | + >>> format_data(1000000) # for self.places = 1 | ||
| 1439 | 1501 | '1.0 M' | |
| 1440 | 1502 | ||
| 1441 | - >>> format_eng(-1e-6) # for self.places = 2 | ||
| 1503 | + >>> format_data(-1e-6) # for self.places = 2 | ||
| 1442 | 1504 | '-1.00 \N{MICRO SIGN}' | |
| 1443 | 1505 | """ | |
| 1444 | 1506 | sign = 1 | |
| 1445 | 1507 | fmt = "g" if self.places is None else f".{self.places:d}f" | |
| 1446 | 1508 | ||
| 1447 | - if num < 0: | ||
| 1509 | + if value < 0: | ||
| 1448 | 1510 | sign = -1 | |
| 1449 | - num = -num | ||
| 1511 | + value = -value | ||
| 1450 | 1512 | ||
| 1451 | - if num != 0: | ||
| 1452 | - pow10 = int(math.floor(math.log10(num) / 3) * 3) | ||
| 1513 | + if value != 0: | ||
| 1514 | + pow10 = int(math.floor(math.log10(value) / 3) * 3) | ||
| 1453 | 1515 | else: | |
| 1454 | 1516 | pow10 = 0 | |
| 1455 | - # Force num to zero, to avoid inconsistencies like | ||
| 1517 | + # Force value to zero, to avoid inconsistencies like | ||
| 1456 | 1518 | # format_eng(-0) = "0" and format_eng(0.0) = "0" | |
| 1457 | 1519 | # but format_eng(-0.0) = "-0.0" | |
| 1458 | - num = 0.0 | ||
| 1520 | + value = 0.0 | ||
| 1459 | 1521 | ||
| 1460 | 1522 | pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) | |
| 1461 | 1523 | ||
| 1462 | - mant = sign * num / (10.0 ** pow10) | ||
| 1524 | + mant = sign * value / (10.0 ** pow10) | ||
| 1463 | 1525 | # Taking care of the cases like 999.9..., which may be rounded to 1000 | |
| 1464 | 1526 | # instead of 1 k. Beware of the corner case of values that are beyond | |
| 1465 | 1527 | # the range of SI prefixes (i.e. > 'Y'). | |
@@ -1468,13 +1530,15 @@ def format_eng(self, num): | |||
| 1468 | 1530 | mant /= 1000 | |
| 1469 | 1531 | pow10 += 3 | |
| 1470 | 1532 | ||
| 1471 | - prefix = self.ENG_PREFIXES[int(pow10)] | ||
| 1533 | + unit_prefix = self.ENG_PREFIXES[int(pow10)] | ||
| 1534 | + if self.unit or unit_prefix: | ||
| 1535 | + suffix = f"{self.sep}{unit_prefix}{self.unit}" | ||
| 1536 | + else: | ||
| 1537 | + suffix = "" | ||
| 1472 | 1538 | if self._usetex or self._useMathText: | |
| 1473 | - formatted = f"${mant:{fmt}}${self.sep}{prefix}" | ||
| 1539 | + return f"${mant:{fmt}}${suffix}" | ||
| 1474 | 1540 | else: | |
| 1475 | - formatted = f"{mant:{fmt}}{self.sep}{prefix}" | ||
| 1476 | - | ||
| 1477 | - return formatted | ||
| 1541 | + return f"{mant:{fmt}}{suffix}" | ||
| 1478 | 1542 | ||
| 1479 | 1543 | ||
| 1480 | 1544 | class PercentFormatter(Formatter): | |
| Back | FazBrowse Home | New Git URL |
0 commit comments