From fbdeaa8820ed54f3f07910965cb3763a27f970c9 Mon Sep 17 00:00:00 2001 From: Brock Date: Sun, 30 Nov 2025 15:48:44 -0800 Subject: [PATCH 1/2] BUG: plotting with non-nano TimedeltaIndex --- pandas/plotting/_matplotlib/converter.py | 16 +++++++++++----- pandas/plotting/_matplotlib/timeseries.py | 2 +- pandas/tests/plotting/test_converter.py | 2 +- pandas/tests/plotting/test_datetimelike.py | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 6ff7f4eda62eb..5d06cbb8e8d2e 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -61,6 +61,7 @@ from matplotlib.axis import Axis from pandas._libs.tslibs.offsets import BaseOffset + from pandas._typing import TimeUnit _mpl_units: dict = {} # Cache for units overwritten by us @@ -1099,18 +1100,22 @@ class TimeSeries_TimedeltaFormatter(mpl.ticker.Formatter): # pyright: ignore[re Formats the ticks along an axis controlled by a :class:`TimedeltaIndex`. """ + def __init__(self, unit: TimeUnit = "ns"): + self.unit = unit + super().__init__() + axis: Axis @staticmethod - def format_timedelta_ticks(x, pos, n_decimals: int) -> str: + def format_timedelta_ticks(x, pos, n_decimals: int, exp: int) -> str: """ Convert seconds to 'D days HH:MM:SS.F' """ - s, ns = divmod(x, 10**9) # TODO(non-nano): this looks like it assumes ns + s, ns = divmod(x, 10**exp) m, s = divmod(s, 60) h, m = divmod(m, 60) d, h = divmod(h, 24) - decimals = int(ns * 10 ** (n_decimals - 9)) + decimals = int(ns * 10 ** (n_decimals - exp)) s = f"{int(h):02d}:{int(m):02d}:{int(s):02d}" if n_decimals > 0: s += f".{decimals:0{n_decimals}d}" @@ -1119,6 +1124,7 @@ def format_timedelta_ticks(x, pos, n_decimals: int) -> str: return s def __call__(self, x, pos: int | None = 0) -> str: + exp = {"ns": 9, "us": 6, "ms": 3, "s": 0}[self.unit] (vmin, vmax) = tuple(self.axis.get_view_interval()) - n_decimals = min(int(np.ceil(np.log10(100 * 10**9 / abs(vmax - vmin)))), 9) - return self.format_timedelta_ticks(x, pos, n_decimals) + n_decimals = min(int(np.ceil(np.log10(100 * 10**exp / abs(vmax - vmin)))), exp) + return self.format_timedelta_ticks(x, pos, n_decimals, exp) diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index e489b6a5e8f30..5023867445adb 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -341,7 +341,7 @@ def format_dateaxis( subplot.format_coord = functools.partial(_format_coord, freq) elif isinstance(index, ABCTimedeltaIndex): - subplot.xaxis.set_major_formatter(TimeSeries_TimedeltaFormatter()) + subplot.xaxis.set_major_formatter(TimeSeries_TimedeltaFormatter(index.unit)) else: raise TypeError("index type not supported") diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index cfdfa7f723599..e33e91ccf6c6e 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -347,7 +347,7 @@ class TestTimeDeltaConverter: ) def test_format_timedelta_ticks(self, x, decimal, format_expected): tdc = converter.TimeSeries_TimedeltaFormatter - result = tdc.format_timedelta_ticks(x, pos=None, n_decimals=decimal) + result = tdc.format_timedelta_ticks(x, pos=None, n_decimals=decimal, exp=9) assert result == format_expected @pytest.mark.parametrize("view_interval", [(1, 2), (2, 1)]) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index ca56185deaebe..5c2a31b8bc548 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1531,7 +1531,7 @@ def test_format_timedelta_ticks_narrow(self): assert len(result_labels) == len(expected_labels) assert result_labels == expected_labels - def test_format_timedelta_ticks_wide(self): + def test_format_timedelta_ticks_wide(self, unit): expected_labels = [ "00:00:00", "1 days 03:46:40", @@ -1544,7 +1544,7 @@ def test_format_timedelta_ticks_wide(self): "9 days 06:13:20", ] - rng = timedelta_range("0", periods=10, freq="1 D") + rng = timedelta_range("0", periods=10, freq="1 D", unit=unit) df = DataFrame(np.random.default_rng(2).standard_normal((len(rng), 3)), rng) _, ax = mpl.pyplot.subplots() ax = df.plot(fontsize=2, ax=ax) From 7bdbffc2aad2e4c1f423138f108ecc68b3d39750 Mon Sep 17 00:00:00 2001 From: Brock Date: Mon, 1 Dec 2025 07:29:28 -0800 Subject: [PATCH 2/2] whatsnew, default exp arg --- doc/source/whatsnew/v3.0.0.rst | 1 + pandas/plotting/_matplotlib/converter.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst index 5db05142aba98..3470b0446ede1 100644 --- a/doc/source/whatsnew/v3.0.0.rst +++ b/doc/source/whatsnew/v3.0.0.rst @@ -1247,6 +1247,7 @@ Plotting - Bug in :meth:`Series.plot` preventing a line and bar from being aligned on the same plot (:issue:`61161`) - Bug in :meth:`Series.plot` preventing a line and scatter plot from being aligned (:issue:`61005`) - Bug in :meth:`Series.plot` with ``kind="pie"`` with :class:`ArrowDtype` (:issue:`59192`) +- Bug in plotting with a :class:`TimedeltaIndex` with non-nanosecond resolution displaying incorrect labels (:issue:`63237`) Groupby/resample/rolling ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 5d06cbb8e8d2e..813bd984cf297 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -1107,7 +1107,7 @@ def __init__(self, unit: TimeUnit = "ns"): axis: Axis @staticmethod - def format_timedelta_ticks(x, pos, n_decimals: int, exp: int) -> str: + def format_timedelta_ticks(x, pos, n_decimals: int, exp: int = 9) -> str: """ Convert seconds to 'D days HH:MM:SS.F' """