Skip to content

Commit 1400305

Browse files
jgoppertjreback
authored andcommitted
ENH: Adds custom plot formatting for TimedeltaIndex.
Author: James Goppert <[email protected]> Author: James Goppert <[email protected]> Closes #8711 Closes #15067 from jgoppert/tdi_plot_fix and squashes the following commits: 945ec14 [James Goppert] Merge branch 'master' into tdi_plot_fix 7db61ec [James Goppert] Create TimeSeries_TimedeltaFormatter. 232efe6 [James Goppert] Fix comment format and exception type for tdi plotting. 4eff697 [James Goppert] Add more time delta series plotting tests. f5f32bc [James Goppert] Link time delta index docs to better matplotlib docs. d588c2c [James Goppert] Fixes test for tdi w/o autofmt_xdate. b6e6a81 [James Goppert] Disables autofmt_xdate testing. c7851e3 [James Goppert] Adjusts tdi test draw calls to try to fix CI issue. 7d28842 [James Goppert] Switch to draw_idle to try to fix bug on xticks update. 3abc310 [James Goppert] Try plt.draw() instead of canvas.draw() to fix issue on osx 3.5. 91954bd [James Goppert] Finished unit test for timedelta plotting. 41ebc85 [James Goppert] Fixes for review comments from #15067. f021cbd [James Goppert] Support nano-second level precision x-axis labels. 5ec65fa [James Goppert] Plot fix for tdi and added more comments. b967d24 [James Goppert] flake8 fixes for tdi plotting. efe5636 [James Goppert] Adds custom plot formatting for TimedeltaIndex.
1 parent f9d7742 commit 1400305

File tree

6 files changed

+154
-23
lines changed

6 files changed

+154
-23
lines changed

doc/source/visualization.rst

+12
Original file line numberDiff line numberDiff line change
@@ -1245,6 +1245,18 @@ in ``pandas.plot_params`` can be used in a `with statement`:
12451245
12461246
plt.close('all')
12471247
1248+
Automatic Date Tick Adjustment
1249+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1250+
1251+
.. versionadded:: 0.20.0
1252+
1253+
``TimedeltaIndex`` now uses the native matplotlib
1254+
tick locator methods, it is useful to call the automatic
1255+
date tick adjustment from matplotlib for figures whose ticklabels overlap.
1256+
1257+
See the :meth:`autofmt_xdate <matplotlib.figure.autofmt_xdate>` method and the
1258+
`matplotlib documentation <http://matplotlib.org/users/recipes.html#fixing-common-date-annoyances>`__ for more.
1259+
12481260
Subplots
12491261
~~~~~~~~
12501262

doc/source/whatsnew/v0.20.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Other enhancements
155155
- ``Series/DataFrame.squeeze()`` have gained the ``axis`` parameter. (:issue:`15339`)
156156
- ``DataFrame.to_excel()`` has a new ``freeze_panes`` parameter to turn on Freeze Panes when exporting to Excel (:issue:`15160`)
157157
- HTML table output skips ``colspan`` or ``rowspan`` attribute if equal to 1. (:issue:`15403`)
158-
158+
- ``pd.TimedeltaIndex`` now has a custom datetick formatter specifically designed for nanosecond level precision (:issue:`8711`)
159159
.. _ISO 8601 duration: https://en.wikipedia.org/wiki/ISO_8601#Durations
160160

161161
.. _whatsnew_0200.api_breaking:

pandas/tests/plotting/test_datetimelike.py

+58
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pandas import Index, Series, DataFrame
1010

1111
from pandas.tseries.index import date_range, bdate_range
12+
from pandas.tseries.tdi import timedelta_range
1213
from pandas.tseries.offsets import DateOffset
1314
from pandas.tseries.period import period_range, Period, PeriodIndex
1415
from pandas.tseries.resample import DatetimeIndex
@@ -1270,6 +1271,63 @@ def test_plot_outofbounds_datetime(self):
12701271
values = [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)]
12711272
self.plt.plot(values)
12721273

1274+
def test_format_timedelta_ticks_narrow(self):
1275+
1276+
expected_labels = [
1277+
'00:00:00.00000000{:d}'.format(i)
1278+
for i in range(10)]
1279+
1280+
rng = timedelta_range('0', periods=10, freq='ns')
1281+
df = DataFrame(np.random.randn(len(rng), 3), rng)
1282+
ax = df.plot(fontsize=2)
1283+
fig = ax.get_figure()
1284+
fig.canvas.draw()
1285+
labels = ax.get_xticklabels()
1286+
self.assertEqual(len(labels), len(expected_labels))
1287+
for l, l_expected in zip(labels, expected_labels):
1288+
self.assertEqual(l.get_text(), l_expected)
1289+
1290+
def test_format_timedelta_ticks_wide(self):
1291+
1292+
expected_labels = [
1293+
'00:00:00',
1294+
'1 days 03:46:40',
1295+
'2 days 07:33:20',
1296+
'3 days 11:20:00',
1297+
'4 days 15:06:40',
1298+
'5 days 18:53:20',
1299+
'6 days 22:40:00',
1300+
'8 days 02:26:40',
1301+
''
1302+
]
1303+
1304+
rng = timedelta_range('0', periods=10, freq='1 d')
1305+
df = DataFrame(np.random.randn(len(rng), 3), rng)
1306+
ax = df.plot(fontsize=2)
1307+
fig = ax.get_figure()
1308+
fig.canvas.draw()
1309+
labels = ax.get_xticklabels()
1310+
self.assertEqual(len(labels), len(expected_labels))
1311+
for l, l_expected in zip(labels, expected_labels):
1312+
self.assertEqual(l.get_text(), l_expected)
1313+
1314+
def test_timedelta_plot(self):
1315+
# test issue #8711
1316+
s = Series(range(5), timedelta_range('1day', periods=5))
1317+
_check_plot_works(s.plot)
1318+
1319+
# test long period
1320+
index = timedelta_range('1 day 2 hr 30 min 10 s',
1321+
periods=10, freq='1 d')
1322+
s = Series(np.random.randn(len(index)), index)
1323+
_check_plot_works(s.plot)
1324+
1325+
# test short period
1326+
index = timedelta_range('1 day 2 hr 30 min 10 s',
1327+
periods=10, freq='1 ns')
1328+
s = Series(np.random.randn(len(index)), index)
1329+
_check_plot_works(s.plot)
1330+
12731331

12741332
def _check_plot_works(f, freq=None, series=None, *args, **kwargs):
12751333
import matplotlib.pyplot as plt

pandas/tools/plotting.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1781,7 +1781,7 @@ def _ts_plot(cls, ax, x, data, style=None, **kwds):
17811781

17821782
lines = cls._plot(ax, data.index, data.values, style=style, **kwds)
17831783
# set date formatter, locators and rescale limits
1784-
format_dateaxis(ax, ax.freq)
1784+
format_dateaxis(ax, ax.freq, data.index)
17851785
return lines
17861786

17871787
def _get_stacking_id(self):

pandas/tseries/converter.py

+30
Original file line numberDiff line numberDiff line change
@@ -1000,3 +1000,33 @@ def __call__(self, x, pos=0):
10001000
else:
10011001
fmt = self.formatdict.pop(x, '')
10021002
return Period(ordinal=int(x), freq=self.freq).strftime(fmt)
1003+
1004+
1005+
class TimeSeries_TimedeltaFormatter(Formatter):
1006+
"""
1007+
Formats the ticks along an axis controlled by a :class:`TimedeltaIndex`.
1008+
"""
1009+
1010+
@staticmethod
1011+
def format_timedelta_ticks(x, pos, n_decimals):
1012+
"""
1013+
Convert seconds to 'D days HH:MM:SS.F'
1014+
"""
1015+
s, ns = divmod(x, 1e9)
1016+
m, s = divmod(s, 60)
1017+
h, m = divmod(m, 60)
1018+
d, h = divmod(h, 24)
1019+
decimals = int(ns * 10**(n_decimals - 9))
1020+
s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s))
1021+
if n_decimals > 0:
1022+
s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals)
1023+
if d != 0:
1024+
s = '{:d} days '.format(int(d)) + s
1025+
return s
1026+
1027+
def __call__(self, x, pos=0):
1028+
(vmin, vmax) = tuple(self.axis.get_view_interval())
1029+
n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin))))
1030+
if n_decimals > 9:
1031+
n_decimals = 9
1032+
return self.format_timedelta_ticks(x, pos, n_decimals)

pandas/tseries/plotting.py

+52-21
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
from pandas.tseries.offsets import DateOffset
1313
import pandas.tseries.frequencies as frequencies
1414
from pandas.tseries.index import DatetimeIndex
15+
from pandas.tseries.period import PeriodIndex
16+
from pandas.tseries.tdi import TimedeltaIndex
1517
from pandas.formats.printing import pprint_thing
1618
import pandas.compat as compat
1719

1820
from pandas.tseries.converter import (TimeSeries_DateLocator,
19-
TimeSeries_DateFormatter)
21+
TimeSeries_DateFormatter,
22+
TimeSeries_TimedeltaFormatter)
2023

2124
# ---------------------------------------------------------------------
2225
# Plotting functions and monkey patches
@@ -49,7 +52,7 @@ def tsplot(series, plotf, ax=None, **kwargs):
4952
lines = plotf(ax, series.index._mpl_repr(), series.values, **kwargs)
5053

5154
# set date formatter, locators and rescale limits
52-
format_dateaxis(ax, ax.freq)
55+
format_dateaxis(ax, ax.freq, series.index)
5356
return lines
5457

5558

@@ -278,8 +281,24 @@ def _maybe_convert_index(ax, data):
278281
# Patch methods for subplot. Only format_dateaxis is currently used.
279282
# Do we need the rest for convenience?
280283

281-
282-
def format_dateaxis(subplot, freq):
284+
def format_timedelta_ticks(x, pos, n_decimals):
285+
"""
286+
Convert seconds to 'D days HH:MM:SS.F'
287+
"""
288+
s, ns = divmod(x, 1e9)
289+
m, s = divmod(s, 60)
290+
h, m = divmod(m, 60)
291+
d, h = divmod(h, 24)
292+
decimals = int(ns * 10**(n_decimals - 9))
293+
s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s))
294+
if n_decimals > 0:
295+
s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals)
296+
if d != 0:
297+
s = '{:d} days '.format(int(d)) + s
298+
return s
299+
300+
301+
def format_dateaxis(subplot, freq, index):
283302
"""
284303
Pretty-formats the date axis (x-axis).
285304
@@ -288,26 +307,38 @@ def format_dateaxis(subplot, freq):
288307
default, changing the limits of the x axis will intelligently change
289308
the positions of the ticks.
290309
"""
291-
majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True,
292-
minor_locator=False,
293-
plot_obj=subplot)
294-
minlocator = TimeSeries_DateLocator(freq, dynamic_mode=True,
295-
minor_locator=True,
296-
plot_obj=subplot)
297-
subplot.xaxis.set_major_locator(majlocator)
298-
subplot.xaxis.set_minor_locator(minlocator)
299-
300-
majformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True,
310+
311+
# handle index specific formatting
312+
# Note: DatetimeIndex does not use this
313+
# interface. DatetimeIndex uses matplotlib.date directly
314+
if isinstance(index, PeriodIndex):
315+
316+
majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True,
301317
minor_locator=False,
302318
plot_obj=subplot)
303-
minformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True,
319+
minlocator = TimeSeries_DateLocator(freq, dynamic_mode=True,
304320
minor_locator=True,
305321
plot_obj=subplot)
306-
subplot.xaxis.set_major_formatter(majformatter)
307-
subplot.xaxis.set_minor_formatter(minformatter)
308-
309-
# x and y coord info
310-
subplot.format_coord = lambda t, y: (
311-
"t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y))
322+
subplot.xaxis.set_major_locator(majlocator)
323+
subplot.xaxis.set_minor_locator(minlocator)
324+
325+
majformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True,
326+
minor_locator=False,
327+
plot_obj=subplot)
328+
minformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True,
329+
minor_locator=True,
330+
plot_obj=subplot)
331+
subplot.xaxis.set_major_formatter(majformatter)
332+
subplot.xaxis.set_minor_formatter(minformatter)
333+
334+
# x and y coord info
335+
subplot.format_coord = lambda t, y: (
336+
"t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y))
337+
338+
elif isinstance(index, TimedeltaIndex):
339+
subplot.xaxis.set_major_formatter(
340+
TimeSeries_TimedeltaFormatter())
341+
else:
342+
raise TypeError('index type not supported')
312343

313344
pylab.draw_if_interactive()

0 commit comments

Comments
 (0)