From efe56363ccc1b5e4508ce5d5e18eeceddb2f3208 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 03:16:23 -0500 Subject: [PATCH 01/15] Adds custom plot formatting for TimedeltaIndex. --- pandas/tools/plotting.py | 2 +- pandas/tseries/plotting.py | 56 +++++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index bd9933b12b580..a0a5c9bf5c003 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -1780,7 +1780,7 @@ def _ts_plot(cls, ax, x, data, style=None, **kwds): lines = cls._plot(ax, data.index, data.values, style=style, **kwds) # set date formatter, locators and rescale limits - format_dateaxis(ax, ax.freq) + format_dateaxis(ax, ax.freq, data.index) return lines def _get_stacking_id(self): diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 89aecf2acc07e..61a5366692752 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -6,12 +6,15 @@ # TODO: Use the fact that axis can have units to simplify the process import numpy as np +import datetime from matplotlib import pylab from pandas.tseries.period import Period from pandas.tseries.offsets import DateOffset import pandas.tseries.frequencies as frequencies from pandas.tseries.index import DatetimeIndex +from pandas.tseries.period import PeriodIndex +from pandas.tseries.tdi import TimedeltaIndex from pandas.formats.printing import pprint_thing import pandas.compat as compat @@ -49,7 +52,7 @@ def tsplot(series, plotf, ax=None, **kwargs): lines = plotf(ax, series.index._mpl_repr(), series.values, **kwargs) # set date formatter, locators and rescale limits - format_dateaxis(ax, ax.freq) + format_dateaxis(ax, ax.freq, series.index) return lines @@ -278,8 +281,11 @@ def _maybe_convert_index(ax, data): # Patch methods for subplot. Only format_dateaxis is currently used. # Do we need the rest for convenience? +def timeTicks(x, pos): + d = datetime.timedelta(seconds=int(x/1e9)) + return str(d) -def format_dateaxis(subplot, freq): +def format_dateaxis(subplot, freq, index): """ Pretty-formats the date axis (x-axis). @@ -288,26 +294,38 @@ def format_dateaxis(subplot, freq): default, changing the limits of the x axis will intelligently change the positions of the ticks. """ - majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, - minor_locator=False, - plot_obj=subplot) - minlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, - minor_locator=True, - plot_obj=subplot) - subplot.xaxis.set_major_locator(majlocator) - subplot.xaxis.set_minor_locator(minlocator) - - majformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True, + + if isinstance(index, PeriodIndex): + + majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, minor_locator=False, plot_obj=subplot) - minformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True, + minlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, minor_locator=True, plot_obj=subplot) - subplot.xaxis.set_major_formatter(majformatter) - subplot.xaxis.set_minor_formatter(minformatter) + subplot.xaxis.set_major_locator(majlocator) + subplot.xaxis.set_minor_locator(minlocator) + + majformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True, + minor_locator=False, + plot_obj=subplot) + minformatter = TimeSeries_DateFormatter(freq, dynamic_mode=True, + minor_locator=True, + plot_obj=subplot) + subplot.xaxis.set_major_formatter(majformatter) + subplot.xaxis.set_minor_formatter(minformatter) + + # x and y coord info + subplot.format_coord = lambda t, y: ( + "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) + + pylab.draw_if_interactive() + + elif isinstance(index, TimedeltaIndex): + from matplotlib import ticker + formatter = ticker.FuncFormatter(timeTicks) + subplot.xaxis.set_major_formatter(formatter) + else: + raise IOError('index type not supported') - # x and y coord info - subplot.format_coord = lambda t, y: ( - "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) - pylab.draw_if_interactive() From b967d24ae32ae3a1c25cc73553e1b43f2d15c176 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 03:19:31 -0500 Subject: [PATCH 02/15] flake8 fixes for tdi plotting. --- pandas/tseries/plotting.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 61a5366692752..10ca98d8ca689 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -282,9 +282,10 @@ def _maybe_convert_index(ax, data): # Do we need the rest for convenience? def timeTicks(x, pos): - d = datetime.timedelta(seconds=int(x/1e9)) + d = datetime.timedelta(seconds=int(x / 1e9)) return str(d) + def format_dateaxis(subplot, freq, index): """ Pretty-formats the date axis (x-axis). @@ -327,5 +328,3 @@ def format_dateaxis(subplot, freq, index): subplot.xaxis.set_major_formatter(formatter) else: raise IOError('index type not supported') - - From 5ec65faf42cfdd2c362cff517c66210a985eb6ca Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 03:26:24 -0500 Subject: [PATCH 03/15] Plot fix for tdi and added more comments. --- pandas/tseries/plotting.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 10ca98d8ca689..abb2416315e2b 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -296,6 +296,7 @@ def format_dateaxis(subplot, freq, index): the positions of the ticks. """ + # handle index specific formatting if isinstance(index, PeriodIndex): majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, @@ -316,15 +317,18 @@ def format_dateaxis(subplot, freq, index): subplot.xaxis.set_major_formatter(majformatter) subplot.xaxis.set_minor_formatter(minformatter) - # x and y coord info - subplot.format_coord = lambda t, y: ( - "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) - - pylab.draw_if_interactive() - elif isinstance(index, TimedeltaIndex): from matplotlib import ticker formatter = ticker.FuncFormatter(timeTicks) subplot.xaxis.set_major_formatter(formatter) else: raise IOError('index type not supported') + + # Note, DatetimeIndex does not use this + # interface. DatetimeIndex uses matplotlib.date directly + + # x and y coord info + subplot.format_coord = lambda t, y: ( + "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) + + pylab.draw_if_interactive() From f021cbdf1bbc2edc5837d48e7f88f940edac2791 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 05:29:04 -0500 Subject: [PATCH 04/15] Support nano-second level precision x-axis labels. --- pandas/tseries/plotting.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index abb2416315e2b..8665bf83d7333 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -6,7 +6,6 @@ # TODO: Use the fact that axis can have units to simplify the process import numpy as np -import datetime from matplotlib import pylab from pandas.tseries.period import Period @@ -281,9 +280,19 @@ def _maybe_convert_index(ax, data): # Patch methods for subplot. Only format_dateaxis is currently used. # Do we need the rest for convenience? -def timeTicks(x, pos): - d = datetime.timedelta(seconds=int(x / 1e9)) - return str(d) +def timeTicks(x, pos, n_decimals): + ''' Convert seconds to 'D days HH:MM:SS.F' ''' + s, ns = divmod(x, 1e9) + m, s = divmod(s, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + decimals = int(ns * 10**(n_decimals - 9)) + s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s)) + if n_decimals > 0: + s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals) + if d != 0: + s = '{:d} days '.format(int(d)) + s + return s def format_dateaxis(subplot, freq, index): @@ -319,7 +328,12 @@ def format_dateaxis(subplot, freq, index): elif isinstance(index, TimedeltaIndex): from matplotlib import ticker - formatter = ticker.FuncFormatter(timeTicks) + (vmin, vmax) = tuple(subplot.xaxis.get_view_interval()) + n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin)))) + if n_decimals > 9: + n_decimals = 9 + formatter = ticker.FuncFormatter( + lambda x, pos: timeTicks(x, pos, n_decimals)) subplot.xaxis.set_major_formatter(formatter) else: raise IOError('index type not supported') From 41ebc85c70f0fe821e9d477960650c6c26507a69 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 10:43:49 -0500 Subject: [PATCH 05/15] Fixes for review comments from #15067. Still need to improve unit testing. --- doc/source/visualization.rst | 10 ++++++++++ doc/source/whatsnew/v0.20.0.txt | 1 + pandas/tests/plotting/test_datetimelike.py | 17 +++++++++++++++++ pandas/tseries/plotting.py | 17 ++++++++--------- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/doc/source/visualization.rst b/doc/source/visualization.rst index e3b186abe53fc..846bb97294ce7 100644 --- a/doc/source/visualization.rst +++ b/doc/source/visualization.rst @@ -1245,6 +1245,16 @@ in ``pandas.plot_params`` can be used in a `with statement`: plt.close('all') +Automatic Date Tick Adjustment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Especially for the ``TimedeltaIndex`` that uses native matplotlib +tick locator methods, it is useful to call the automatic +date tick adjustment from matplotlib for figures whose ticklabels overlap. + +See the :meth:`autofmt_xdate ` method and the +`matplotlib documentation `__ for more. + Subplots ~~~~~~~~ diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 5c26bb2985e75..b4f791fce9e88 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -117,6 +117,7 @@ Other enhancements - ``.select_dtypes()`` now allows the string 'datetimetz' to generically select datetimes with tz (:issue:`14910`) +- ``pd.TimedeltaIndex`` now has a custom datetick formatter specifically designed for nanosecond level precision (:issue:`8711`) .. _whatsnew_0200.api_breaking: diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index f07aadba175f2..2d789351723c0 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -7,6 +7,7 @@ from pandas import Index, Series, DataFrame from pandas.tseries.index import date_range, bdate_range +from pandas.tseries.tdi import timedelta_range from pandas.tseries.offsets import DateOffset from pandas.tseries.period import period_range, Period, PeriodIndex from pandas.tseries.resample import DatetimeIndex @@ -1271,6 +1272,22 @@ def test_plot_outofbounds_datetime(self): values = [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)] self.plt.plot(values) + def test_format_timedelta_ticks(self): + import matplotlib.pyplot as plt + rng = timedelta_range('0', periods=3, freq='ns') + df = DataFrame(np.random.randn(len(rng), 3), rng) + ax = df.plot() + plt.gcf().autofmt_xdate() + xaxis = ax.get_xaxis() + for l in xaxis.get_ticklabels(): + if len(l.get_text()) > 0: + self.assertEqual(l.get_rotation(), 30) + + def test_timedelta_plot(self): + # test issue #8711 + s = Series(range(5), timedelta_range('1day', periods=5)) + _check_plot_works(s.plot) + def _check_plot_works(f, freq=None, series=None, *args, **kwargs): import matplotlib.pyplot as plt diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 8665bf83d7333..062c5206bb4fa 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -280,7 +280,7 @@ def _maybe_convert_index(ax, data): # Patch methods for subplot. Only format_dateaxis is currently used. # Do we need the rest for convenience? -def timeTicks(x, pos, n_decimals): +def format_timedelta_ticks(x, pos, n_decimals): ''' Convert seconds to 'D days HH:MM:SS.F' ''' s, ns = divmod(x, 1e9) m, s = divmod(s, 60) @@ -306,6 +306,8 @@ def format_dateaxis(subplot, freq, index): """ # handle index specific formatting + # Note: DatetimeIndex does not use this + # interface. DatetimeIndex uses matplotlib.date directly if isinstance(index, PeriodIndex): majlocator = TimeSeries_DateLocator(freq, dynamic_mode=True, @@ -326,6 +328,10 @@ def format_dateaxis(subplot, freq, index): subplot.xaxis.set_major_formatter(majformatter) subplot.xaxis.set_minor_formatter(minformatter) + # x and y coord info + subplot.format_coord = lambda t, y: ( + "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) + elif isinstance(index, TimedeltaIndex): from matplotlib import ticker (vmin, vmax) = tuple(subplot.xaxis.get_view_interval()) @@ -333,16 +339,9 @@ def format_dateaxis(subplot, freq, index): if n_decimals > 9: n_decimals = 9 formatter = ticker.FuncFormatter( - lambda x, pos: timeTicks(x, pos, n_decimals)) + lambda x, pos: format_timedelta_ticks(x, pos, n_decimals)) subplot.xaxis.set_major_formatter(formatter) else: raise IOError('index type not supported') - # Note, DatetimeIndex does not use this - # interface. DatetimeIndex uses matplotlib.date directly - - # x and y coord info - subplot.format_coord = lambda t, y: ( - "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) - pylab.draw_if_interactive() From 91954bd2ee155b7d30dfef7e28074256e9b59369 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 12:20:34 -0500 Subject: [PATCH 06/15] Finished unit test for timedelta plotting. --- pandas/tests/plotting/test_datetimelike.py | 47 ++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 2d789351723c0..798ee0932d9e5 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1272,16 +1272,49 @@ def test_plot_outofbounds_datetime(self): values = [datetime(1677, 1, 1, 12), datetime(1677, 1, 2, 12)] self.plt.plot(values) - def test_format_timedelta_ticks(self): + def test_format_timedelta_ticks_narrow(self): import matplotlib.pyplot as plt - rng = timedelta_range('0', periods=3, freq='ns') + + expected_labels = [ + '00:00:00.00000000{:d}'.format(i) + for i in range(10)] + + rng = timedelta_range('0', periods=10, freq='ns') df = DataFrame(np.random.randn(len(rng), 3), rng) - ax = df.plot() + ax = df.plot(fontsize=2) + ax.get_figure().canvas.draw() plt.gcf().autofmt_xdate() - xaxis = ax.get_xaxis() - for l in xaxis.get_ticklabels(): - if len(l.get_text()) > 0: - self.assertEqual(l.get_rotation(), 30) + labels = ax.get_xticklabels() + self.assertEqual(len(labels), len(expected_labels)) + for l, l_expected in zip(labels, expected_labels): + self.assertEqual(l.get_text(), l_expected) + self.assertEqual(l.get_rotation(), 30) + + def test_format_timedelta_ticks_wide(self): + import matplotlib.pyplot as plt + + expected_labels = [ + '00:00:00', + '1 days 03:46:40', + '2 days 07:33:20', + '3 days 11:20:00', + '4 days 15:06:40', + '5 days 18:53:20', + '6 days 22:40:00', + '8 days 02:26:40', + '' + ] + + rng = timedelta_range('0', periods=10, freq='1 d') + df = DataFrame(np.random.randn(len(rng), 3), rng) + ax = df.plot(fontsize=2) + ax.get_figure().canvas.draw() + plt.gcf().autofmt_xdate() + labels = ax.get_xticklabels() + self.assertEqual(len(labels), len(expected_labels)) + for l, l_expected in zip(labels, expected_labels): + self.assertEqual(l.get_text(), l_expected) + self.assertEqual(l.get_rotation(), 30) def test_timedelta_plot(self): # test issue #8711 From 3abc3102d7cf8c6b0de9d1ba0a1f4d94c7af2131 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 14:16:02 -0500 Subject: [PATCH 07/15] Try plt.draw() instead of canvas.draw() to fix issue on osx 3.5. --- pandas/tests/plotting/test_datetimelike.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 798ee0932d9e5..cc6cb7b5c6f75 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1282,7 +1282,7 @@ def test_format_timedelta_ticks_narrow(self): rng = timedelta_range('0', periods=10, freq='ns') df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) - ax.get_figure().canvas.draw() + plt.draw() plt.gcf().autofmt_xdate() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) @@ -1308,7 +1308,7 @@ def test_format_timedelta_ticks_wide(self): rng = timedelta_range('0', periods=10, freq='1 d') df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) - ax.get_figure().canvas.draw() + plt.draw() plt.gcf().autofmt_xdate() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) From 7d288420dad60dc777c7c8bd9029ebef12d4aa97 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 14:42:05 -0500 Subject: [PATCH 08/15] Switch to draw_idle to try to fix bug on xticks update. --- pandas/tests/plotting/test_datetimelike.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index cc6cb7b5c6f75..cff7ba957db6a 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1282,8 +1282,8 @@ def test_format_timedelta_ticks_narrow(self): rng = timedelta_range('0', periods=10, freq='ns') df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) - plt.draw() plt.gcf().autofmt_xdate() + ax.get_figure().canvas.draw_idle() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) for l, l_expected in zip(labels, expected_labels): @@ -1308,8 +1308,8 @@ def test_format_timedelta_ticks_wide(self): rng = timedelta_range('0', periods=10, freq='1 d') df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) - plt.draw() plt.gcf().autofmt_xdate() + ax.get_figure().canvas.draw_idle() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) for l, l_expected in zip(labels, expected_labels): From c7851e30f8766dced7f76b5cbe9b24f4166f98c6 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 15:59:24 -0500 Subject: [PATCH 09/15] Adjusts tdi test draw calls to try to fix CI issue. --- pandas/tests/plotting/test_datetimelike.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index cff7ba957db6a..698e68befb6f2 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1273,7 +1273,6 @@ def test_plot_outofbounds_datetime(self): self.plt.plot(values) def test_format_timedelta_ticks_narrow(self): - import matplotlib.pyplot as plt expected_labels = [ '00:00:00.00000000{:d}'.format(i) @@ -1282,8 +1281,9 @@ def test_format_timedelta_ticks_narrow(self): rng = timedelta_range('0', periods=10, freq='ns') df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) - plt.gcf().autofmt_xdate() - ax.get_figure().canvas.draw_idle() + fig = ax.get_figure() + fig.autofmt_xdate() + fig.canvas.draw() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) for l, l_expected in zip(labels, expected_labels): @@ -1291,7 +1291,6 @@ def test_format_timedelta_ticks_narrow(self): self.assertEqual(l.get_rotation(), 30) def test_format_timedelta_ticks_wide(self): - import matplotlib.pyplot as plt expected_labels = [ '00:00:00', @@ -1308,8 +1307,9 @@ def test_format_timedelta_ticks_wide(self): rng = timedelta_range('0', periods=10, freq='1 d') df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) - plt.gcf().autofmt_xdate() - ax.get_figure().canvas.draw_idle() + fig = ax.get_figure() + fig.autofmt_xdate() + fig.canvas.draw() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) for l, l_expected in zip(labels, expected_labels): From b6e6a8162043a25a950c88bcb6418ce3b41804e6 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 17:27:33 -0500 Subject: [PATCH 10/15] Disables autofmt_xdate testing. --- pandas/tests/plotting/test_datetimelike.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 698e68befb6f2..530f964dc9c0d 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1282,7 +1282,7 @@ def test_format_timedelta_ticks_narrow(self): df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) fig = ax.get_figure() - fig.autofmt_xdate() + #fig.autofmt_xdate() fig.canvas.draw() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) @@ -1308,7 +1308,7 @@ def test_format_timedelta_ticks_wide(self): df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) fig = ax.get_figure() - fig.autofmt_xdate() + #fig.autofmt_xdate() fig.canvas.draw() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) From d588c2c6b62f1e33a7df52051d4ef47c58e4a77b Mon Sep 17 00:00:00 2001 From: James Goppert Date: Thu, 5 Jan 2017 18:10:17 -0500 Subject: [PATCH 11/15] Fixes test for tdi w/o autofmt_xdate. --- pandas/tests/plotting/test_datetimelike.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 530f964dc9c0d..7c42d1843c0ee 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1282,13 +1282,11 @@ def test_format_timedelta_ticks_narrow(self): df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) fig = ax.get_figure() - #fig.autofmt_xdate() fig.canvas.draw() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) for l, l_expected in zip(labels, expected_labels): self.assertEqual(l.get_text(), l_expected) - self.assertEqual(l.get_rotation(), 30) def test_format_timedelta_ticks_wide(self): @@ -1308,13 +1306,11 @@ def test_format_timedelta_ticks_wide(self): df = DataFrame(np.random.randn(len(rng), 3), rng) ax = df.plot(fontsize=2) fig = ax.get_figure() - #fig.autofmt_xdate() fig.canvas.draw() labels = ax.get_xticklabels() self.assertEqual(len(labels), len(expected_labels)) for l, l_expected in zip(labels, expected_labels): self.assertEqual(l.get_text(), l_expected) - self.assertEqual(l.get_rotation(), 30) def test_timedelta_plot(self): # test issue #8711 From f5f32bc99aadbe67e8c967b7f360bdad6a1ba0a7 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Sat, 14 Jan 2017 16:45:01 -0500 Subject: [PATCH 12/15] Link time delta index docs to better matplotlib docs. --- doc/source/visualization.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/visualization.rst b/doc/source/visualization.rst index 846bb97294ce7..6f4ead92c5987 100644 --- a/doc/source/visualization.rst +++ b/doc/source/visualization.rst @@ -1253,7 +1253,7 @@ tick locator methods, it is useful to call the automatic date tick adjustment from matplotlib for figures whose ticklabels overlap. See the :meth:`autofmt_xdate ` method and the -`matplotlib documentation `__ for more. +`matplotlib documentation `__ for more. Subplots ~~~~~~~~ From 4eff697702f5e18269ef42ab27db2566118fb4bb Mon Sep 17 00:00:00 2001 From: James Goppert Date: Sat, 14 Jan 2017 16:45:29 -0500 Subject: [PATCH 13/15] Add more time delta series plotting tests. --- pandas/tests/plotting/test_datetimelike.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index 7c42d1843c0ee..40ff6b205c464 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1317,6 +1317,18 @@ def test_timedelta_plot(self): s = Series(range(5), timedelta_range('1day', periods=5)) _check_plot_works(s.plot) + # test long period + index = timedelta_range('1 day 2 hr 30 min 10 s', + periods=10, freq='1 d') + s = Series(np.random.randn(len(index)), index) + _check_plot_works(s.plot) + + # test short period + index = timedelta_range('1 day 2 hr 30 min 10 s', + periods=10, freq='1 ns') + s = Series(np.random.randn(len(index)), index) + _check_plot_works(s.plot) + def _check_plot_works(f, freq=None, series=None, *args, **kwargs): import matplotlib.pyplot as plt From 232efe66a1e827bdd4627a11a2d84ffcda9b7cfd Mon Sep 17 00:00:00 2001 From: James Goppert Date: Sat, 14 Jan 2017 17:14:56 -0500 Subject: [PATCH 14/15] Fix comment format and exception type for tdi plotting. --- pandas/tseries/plotting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 062c5206bb4fa..5aaee83829418 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -281,7 +281,9 @@ def _maybe_convert_index(ax, data): # Do we need the rest for convenience? def format_timedelta_ticks(x, pos, n_decimals): - ''' Convert seconds to 'D days HH:MM:SS.F' ''' + """ + Convert seconds to 'D days HH:MM:SS.F' + """ s, ns = divmod(x, 1e9) m, s = divmod(s, 60) h, m = divmod(m, 60) @@ -342,6 +344,6 @@ def format_dateaxis(subplot, freq, index): lambda x, pos: format_timedelta_ticks(x, pos, n_decimals)) subplot.xaxis.set_major_formatter(formatter) else: - raise IOError('index type not supported') + raise TypeError('index type not supported') pylab.draw_if_interactive() From 7db61ec1947309bccb2feaa84b8e56a478baba71 Mon Sep 17 00:00:00 2001 From: James Goppert Date: Sat, 14 Jan 2017 17:42:54 -0500 Subject: [PATCH 15/15] Create TimeSeries_TimedeltaFormatter. --- pandas/tseries/converter.py | 30 ++++++++++++++++++++++++++++++ pandas/tseries/plotting.py | 13 ++++--------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/pandas/tseries/converter.py b/pandas/tseries/converter.py index 95ff9578fa3ee..db7049ebc89b3 100644 --- a/pandas/tseries/converter.py +++ b/pandas/tseries/converter.py @@ -1000,3 +1000,33 @@ def __call__(self, x, pos=0): else: fmt = self.formatdict.pop(x, '') return Period(ordinal=int(x), freq=self.freq).strftime(fmt) + + +class TimeSeries_TimedeltaFormatter(Formatter): + """ + Formats the ticks along an axis controlled by a :class:`TimedeltaIndex`. + """ + + @staticmethod + def format_timedelta_ticks(x, pos, n_decimals): + """ + Convert seconds to 'D days HH:MM:SS.F' + """ + s, ns = divmod(x, 1e9) + m, s = divmod(s, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + decimals = int(ns * 10**(n_decimals - 9)) + s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s)) + if n_decimals > 0: + s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals) + if d != 0: + s = '{:d} days '.format(int(d)) + s + return s + + def __call__(self, x, pos=0): + (vmin, vmax) = tuple(self.axis.get_view_interval()) + n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin)))) + if n_decimals > 9: + n_decimals = 9 + return self.format_timedelta_ticks(x, pos, n_decimals) diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index 5aaee83829418..4eddf54701889 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -18,7 +18,8 @@ import pandas.compat as compat from pandas.tseries.converter import (TimeSeries_DateLocator, - TimeSeries_DateFormatter) + TimeSeries_DateFormatter, + TimeSeries_TimedeltaFormatter) # --------------------------------------------------------------------- # Plotting functions and monkey patches @@ -335,14 +336,8 @@ def format_dateaxis(subplot, freq, index): "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)) elif isinstance(index, TimedeltaIndex): - from matplotlib import ticker - (vmin, vmax) = tuple(subplot.xaxis.get_view_interval()) - n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin)))) - if n_decimals > 9: - n_decimals = 9 - formatter = ticker.FuncFormatter( - lambda x, pos: format_timedelta_ticks(x, pos, n_decimals)) - subplot.xaxis.set_major_formatter(formatter) + subplot.xaxis.set_major_formatter( + TimeSeries_TimedeltaFormatter()) else: raise TypeError('index type not supported')