Skip to content

[ENH]: EngFormatter to print as significant figures #30727

@cparg

Description

@cparg

Problem

  • It is common in Engineering to print numbers with a given amount of significant figures. That means total amount of figures in a number.
  • The Exponent is a multiple of three encoded as a letter suffix (as EngFormatter is already implementing).
  • matplotlib.ticker.EngFormatter uses the argument "places" as figures behind the decimal points
  • the resulting representation suggests a variable accuracy of the value when using a multiple 3 for the exponent, which is bad
  • this also makes the string representation varying in length, which is also bad.

Examples:

  • for places=4 expected 12.35 M ; actual: 12.3457 M
  • for places=5 expected 12.346 M; actual: 12.34568 M
  • for places=4 expected 123.5 µ; actual: 123.4568 µ
  • for places=5 expected 12.346 m ; actual: 12.34568 m

Proposed solution

  • This overloaded class illustrates the desired formatting :
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import math
import numpy as np
from matplotlib.ticker import EngFormatter

class eng(EngFormatter):

    def format_eng(self, num):
        """
        Format a number in engineering notation, appending a letter
        representing the power of 1000 of the original number.
        Some examples:

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

        >>> format_eng(1000000)  # for self.places = 1
        '1.0 M'

        >>> format_eng(-1e-6)  # for self.places = 2
        '-1.00 \N{MICRO SIGN}'
        """
        sign = 1

        if num < 0:
            sign = -1
            num = -num

        if num != 0:
            pow10 = int(math.floor(math.log10(num) / 3) * 3)
        else:
            pow10 = 0
            # Force num to zero, to avoid inconsistencies like
            # format_eng(-0) = "0" and format_eng(0.0) = "0"
            # but format_eng(-0.0) = "-0.0"
            num = 0.0

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

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

        if self.places is None:
            fmt = "g"
        else:
            if self.places < 3:
                raise ValueError("places must be at least 3")
            mant_len = len(str(int(mant)))
            fmt = f".{self.places - mant_len:d}f"

        # Taking care of the cases like 999.9..., which may be rounded to 1000
        # instead of 1 k.  Beware of the corner case of values that are beyond
        # the range of SI prefixes (i.e. > 'Y').
        if (abs(float(format(mant, fmt))) >= 1000
                and pow10 < max(self.ENG_PREFIXES)):
            mant /= 1000
            pow10 += 3
            fmt = f".{self.places - len(str(int(mant))):d}f"


        prefix = self.ENG_PREFIXES[int(pow10)]
        if self._usetex or self._useMathText:
            formatted = f"${mant:{fmt}}${self.sep}{prefix}"
        else:
            formatted = f"{mant:{fmt}}{self.sep}{prefix}"

        return formatted


def test_eng_formatter():
    newFormatter=True 
    if newFormatter:
        print("Using patched eng formatter")
        formatter3 = eng(places=3)
        formatter4 = eng(places=4)
        formatter5 = eng(places=5)
    else:
        print("Using matplotlib.ticker.EngFormatter")
        formatter3 = EngFormatter(places=3)
        formatter4 = EngFormatter(places=4)
        formatter5 = EngFormatter(places=5)

    print(formatter4(12_345_678.90))
    print(formatter5(12_345_678.90))
    print(formatter4(0.0001234567890))
    print(formatter5(0.01234567890))
    print(formatter3(999.9))
    print(formatter4(999.9))
    print(formatter5(999.9))

    if newFormatter:
        assert formatter4(12_345_678.90)   == "12.35 M"  
        assert formatter5(12_345_678.90)   == "12.346 M"
        assert formatter4(0.0001234567890) == "123.5 µ"
        assert formatter5(0.01234567890)   == "12.346 m"

test_eng_formatter()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions