diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 9dc10a09378f8..624a77b33fd41 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -297,6 +297,7 @@ Plotting ^^^^^^^^ - :func: `DataFrame.plot` now raises a ``ValueError`` when the ``x`` or ``y`` argument is improperly formed (:issue:`18671`) +- Bug in formatting tick labels with datetime.time() and fractional seconds (:issue:`18478`). - - diff --git a/pandas/plotting/_converter.py b/pandas/plotting/_converter.py index 3a3d871c043c4..2a45e20dda4cc 100644 --- a/pandas/plotting/_converter.py +++ b/pandas/plotting/_converter.py @@ -190,19 +190,39 @@ def __init__(self, locs): self.locs = locs def __call__(self, x, pos=0): - fmt = '%H:%M:%S' + """ + Return the time of day as a formatted string. + + Parameters + ---------- + x : float + The time of day specified as seconds since 00:00 (midnight), + with upto microsecond precision. + pos + Unused + + Returns + ------- + str + A string in HH:MM:SS.mmmuuu format. Microseconds, + milliseconds and seconds are only displayed if non-zero. + """ + fmt = '%H:%M:%S.%f' s = int(x) - ms = int((x - s) * 1e3) - us = int((x - s) * 1e6 - ms) + msus = int(round((x - s) * 1e6)) + ms = msus // 1000 + us = msus % 1000 m, s = divmod(s, 60) h, m = divmod(m, 60) _, h = divmod(h, 24) if us != 0: - fmt += '.%6f' + return pydt.time(h, m, s, msus).strftime(fmt) elif ms != 0: - fmt += '.%3f' + return pydt.time(h, m, s, msus).strftime(fmt)[:-3] + elif s != 0: + return pydt.time(h, m, s).strftime('%H:%M:%S') - return pydt.time(h, m, s, us).strftime(fmt) + return pydt.time(h, m).strftime('%H:%M') # Period Conversion diff --git a/pandas/tests/plotting/test_converter.py b/pandas/tests/plotting/test_converter.py index 3818c04649366..cc275282436c8 100644 --- a/pandas/tests/plotting/test_converter.py +++ b/pandas/tests/plotting/test_converter.py @@ -236,7 +236,28 @@ def test_conversion_outofbounds_datetime(self): assert rs == xp def test_time_formatter(self): - self.tc(90000) + # issue 18478 + + # time2num(datetime.time.min) + rs = self.tc(0) + xp = '00:00' + assert rs == xp + + # time2num(datetime.time.max) + rs = self.tc(86399.999999) + xp = '23:59:59.999999' + assert rs == xp + + # some other times + rs = self.tc(90000) + xp = '01:00' + assert rs == xp + rs = self.tc(3723) + xp = '01:02:03' + assert rs == xp + rs = self.tc(39723.2) + xp = '11:02:03.200' + assert rs == xp def test_dateindex_conversion(self): decimals = 9 diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index e87c67a682d46..aa27cb0f4ee1c 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1032,32 +1032,40 @@ def test_time(self): df = DataFrame({'a': np.random.randn(len(ts)), 'b': np.random.randn(len(ts))}, index=ts) - _, ax = self.plt.subplots() + fig, ax = self.plt.subplots() df.plot(ax=ax) # verify tick labels + fig.canvas.draw() ticks = ax.get_xticks() labels = ax.get_xticklabels() for t, l in zip(ticks, labels): m, s = divmod(int(t), 60) h, m = divmod(m, 60) - xp = l.get_text() - if len(xp) > 0: - rs = time(h, m, s).strftime('%H:%M:%S') + rs = l.get_text() + if len(rs) > 0: + if s != 0: + xp = time(h, m, s).strftime('%H:%M:%S') + else: + xp = time(h, m, s).strftime('%H:%M') assert xp == rs # change xlim ax.set_xlim('1:30', '5:00') # check tick labels again + fig.canvas.draw() ticks = ax.get_xticks() labels = ax.get_xticklabels() for t, l in zip(ticks, labels): m, s = divmod(int(t), 60) h, m = divmod(m, 60) - xp = l.get_text() - if len(xp) > 0: - rs = time(h, m, s).strftime('%H:%M:%S') + rs = l.get_text() + if len(rs) > 0: + if s != 0: + xp = time(h, m, s).strftime('%H:%M:%S') + else: + xp = time(h, m, s).strftime('%H:%M') assert xp == rs @pytest.mark.slow @@ -1069,22 +1077,29 @@ def test_time_musec(self): df = DataFrame({'a': np.random.randn(len(ts)), 'b': np.random.randn(len(ts))}, index=ts) - _, ax = self.plt.subplots() + fig, ax = self.plt.subplots() ax = df.plot(ax=ax) # verify tick labels + fig.canvas.draw() ticks = ax.get_xticks() labels = ax.get_xticklabels() for t, l in zip(ticks, labels): m, s = divmod(int(t), 60) - # TODO: unused? - # us = int((t - int(t)) * 1e6) + us = int(round((t - int(t)) * 1e6)) h, m = divmod(m, 60) - xp = l.get_text() - if len(xp) > 0: - rs = time(h, m, s).strftime('%H:%M:%S.%f') + rs = l.get_text() + if len(rs) > 0: + if (us % 1000) != 0: + xp = time(h, m, s, us).strftime('%H:%M:%S.%f') + elif (us // 1000) != 0: + xp = time(h, m, s, us).strftime('%H:%M:%S.%f')[:-3] + elif s != 0: + xp = time(h, m, s, us).strftime('%H:%M:%S') + else: + xp = time(h, m, s, us).strftime('%H:%M') assert xp == rs @pytest.mark.slow