-
-
Notifications
You must be signed in to change notification settings - Fork 8.1k
ENH: Add digits parameter to EngFormatter for significant figures #30827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
27fb34d
1bbde85
4403ad9
fcd5e10
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| ``EngFormatter`` significant digits support | ||
| -------------------------------------------- | ||
|
|
||
| The `.ticker.EngFormatter` now supports a ``digits`` parameter to format | ||
| tick labels using a fixed number of significant figures across magnitudes, | ||
| which is a common requirement in scientific and engineering plots where | ||
| values at very different scales should convey comparable precision. | ||
|
|
||
| Previously, only the ``places`` parameter was available. That option fixes | ||
| the number of decimal places, which can implicitly change the number of | ||
| significant figures as the prefix changes. For example, with ``places=1``, | ||
| the formatter would produce "10.0 Hz" (3 significant figures) but | ||
| "100.0 MHz" (4 significant figures) for similarly precise values. | ||
|
|
||
| The new ``digits`` parameter ensures consistent precision regardless of | ||
| magnitude: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| import matplotlib.pyplot as plt | ||
| import matplotlib.ticker as mticker | ||
|
|
||
| formatter = mticker.EngFormatter(unit='V', digits=4) | ||
| # All values display exactly 4 significant figures | ||
| formatter.format_data(12345) # "12.35 kV" | ||
| formatter.format_data(123.45) # "123.5 V" | ||
| formatter.format_data(0.001234) # "1.234 mV" | ||
|
|
||
| In addition, a ``trim_zeros`` parameter controls whether trailing zeros are | ||
| preserved (``trim_zeros="keep"``, the default, preserving current behavior) | ||
| or removed (``trim_zeros="trim"``) for a more compact visual presentation | ||
| in tick labels. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1369,8 +1369,9 @@ class EngFormatter(ScalarFormatter): | |
| 30: "Q" | ||
| } | ||
|
|
||
| def __init__(self, unit="", places=None, sep=" ", *, usetex=None, | ||
| useMathText=None, useOffset=False): | ||
| def __init__(self, unit="", places=None, sep=" ", *, | ||
| digits=None, trim_zeros="keep", | ||
| usetex=None, useMathText=None, useOffset=False): | ||
| r""" | ||
| Parameters | ||
| ---------- | ||
|
|
@@ -1397,6 +1398,33 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, | |
| * ``sep="\N{NARROW NO-BREAK SPACE}"`` (``U+202F``); | ||
| * ``sep="\N{NO-BREAK SPACE}"`` (``U+00A0``). | ||
|
|
||
| digits : int, default: None | ||
| Number of significant digits to display in the mantissa. | ||
| This provides consistent precision across different magnitudes. | ||
| For example, with ``digits=4``, the values 12345, 123.45, and | ||
| 0.001234 will be formatted as "12.35 k", "123.5", and "1.234 m" | ||
| respectively, all displaying exactly 4 significant figures. | ||
| Mutually exclusive with *places*. | ||
|
|
||
| If neither *places* nor *digits* is specified, the formatter | ||
| uses Python's '%g' format, which displays up to 6 significant | ||
| digits. | ||
|
|
||
| .. versionadded:: 3.11 | ||
|
|
||
| trim_zeros : {"keep", "trim"}, default: "keep" | ||
| Whether to remove trailing zeros from the mantissa after formatting. | ||
|
|
||
| ``"keep"``: Preserve trailing zeros to maintain the specified | ||
| precision (e.g., "12.300 k" with ``digits=5``). | ||
| ``"trim"``: Remove trailing zeros for cleaner appearance | ||
| (e.g., "12.3 k" with ``digits=5``). | ||
|
|
||
| The default ``"keep"`` preserves mathematical significance, as | ||
| trailing zeros indicate the precision of the measurement. | ||
|
|
||
| .. versionadded:: 3.11 | ||
|
|
||
| usetex : bool, default: :rc:`text.usetex` | ||
| To enable/disable the use of TeX's math mode for rendering the | ||
| numbers in the formatter. | ||
|
|
@@ -1414,8 +1442,21 @@ def __init__(self, unit="", places=None, sep=" ", *, usetex=None, | |
|
|
||
| .. versionadded:: 3.10 | ||
| """ | ||
| if places is not None and digits is not None: | ||
| raise ValueError( | ||
| "Cannot specify 'places' and 'digits'" | ||
| ) | ||
|
|
||
| if trim_zeros not in ("keep", "trim"): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to not make
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially chose string values to make the intent more explicit in code (e.g., |
||
| raise ValueError( | ||
| f"trim_zeros must be 'keep' or 'trim', got {trim_zeros!r}" | ||
| ) | ||
|
|
||
| self.unit = unit | ||
| self.places = places | ||
| self.digits = digits | ||
| self.trim_zeros = trim_zeros | ||
|
|
||
| self.sep = sep | ||
| super().__init__( | ||
| useOffset=useOffset, | ||
|
|
@@ -1504,7 +1545,6 @@ def format_data(self, value): | |
| '-1.00 \N{MICRO SIGN}' | ||
| """ | ||
| sign = 1 | ||
| fmt = "g" if self.places is None else f".{self.places:d}f" | ||
|
|
||
| if value < 0: | ||
| sign = -1 | ||
|
|
@@ -1522,23 +1562,61 @@ def format_data(self, value): | |
| pow10 = np.clip(pow10, min(self.ENG_PREFIXES), max(self.ENG_PREFIXES)) | ||
|
|
||
| mant = sign * value / (10.0 ** pow10) | ||
|
|
||
| # Determine format string based on digits or places | ||
| if self.digits is not None: | ||
| # Significant figures formatting | ||
| if mant == 0: | ||
| mant_int_digits = 1 | ||
| else: | ||
| mant_int_digits = int(math.floor(math.log10(abs(mant)))) + 1 | ||
|
|
||
| # Calculate decimal places needed to achieve desired sig figs | ||
| decimal_places = max(0, self.digits - mant_int_digits) | ||
| fmt = f".{decimal_places}f" | ||
| elif self.places is not None: | ||
| # Original behavior | ||
| fmt = f".{self.places:d}f" | ||
| else: | ||
| fmt = "g" | ||
|
|
||
| # Format the mantissa | ||
| formatted_mant = format(mant, fmt) | ||
|
|
||
| # 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 | ||
| if (abs(float(formatted_mant)) >= 1000 | ||
| and pow10 < max(self.ENG_PREFIXES)): | ||
| mant /= 1000 | ||
| pow10 += 3 | ||
|
|
||
| # Reformat with adjusted exponent | ||
| if self.digits is not None: | ||
| # After dividing by 1000, we have 1 digit before decimal | ||
| mant_int_digits = 1 | ||
| decimal_places = max(0, self.digits - mant_int_digits) | ||
| fmt = f".{decimal_places}f" | ||
| elif self.places is not None: | ||
| fmt = f".{self.places:d}f" | ||
| else: | ||
| fmt = "g" | ||
|
|
||
| formatted_mant = format(mant, fmt) | ||
|
|
||
| # Trim trailing zeros if requested | ||
| if self.trim_zeros == "trim" and "." in formatted_mant: | ||
| formatted_mant = formatted_mant.rstrip("0").rstrip(".") | ||
|
|
||
| unit_prefix = self.ENG_PREFIXES[int(pow10)] | ||
| if self.unit or unit_prefix: | ||
| suffix = f"{self.sep}{unit_prefix}{self.unit}" | ||
| else: | ||
| suffix = "" | ||
| if self._usetex or self._useMathText: | ||
| return f"${mant:{fmt}}${suffix}" | ||
| return f"${formatted_mant}${suffix}" | ||
| else: | ||
| return f"{mant:{fmt}}{suffix}" | ||
| return f"{formatted_mant}{suffix}" | ||
|
|
||
|
|
||
| class PercentFormatter(Formatter): | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.