Skip to content

BUG: Fix TimeFormatter behavior with fractional seconds #18552

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

Closed
wants to merge 11 commits into from
Closed
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.22.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
-
-

Expand Down
32 changes: 26 additions & 6 deletions pandas/plotting/_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,19 +190,39 @@ def __init__(self, locs):
self.locs = locs

def __call__(self, x, pos=0):
fmt = '%H:%M:%S'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a doc-string here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cam you add a Parameters sections

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and a Returns

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"""
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
Expand Down
23 changes: 22 additions & 1 deletion pandas/tests/plotting/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,28 @@ def test_conversion_outofbounds_datetime(self):
assert rs == xp

def test_time_formatter(self):
self.tc(90000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the issue number here as a comment

# 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
Expand Down
41 changes: 28 additions & 13 deletions pandas/tests/plotting/test_datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down