From e42513b1d99c6d44d0352b0dd98619be32abe837 Mon Sep 17 00:00:00 2001 From: sinhrks Date: Sat, 5 Jul 2014 17:09:36 +0900 Subject: [PATCH] BUG/CLN: Refactoring tsplot --- pandas/tools/plotting.py | 46 ++++---- pandas/tseries/plotting.py | 162 ++++++++++++++++---------- pandas/tseries/tests/test_plotting.py | 66 +++++++---- 3 files changed, 164 insertions(+), 110 deletions(-) diff --git a/pandas/tools/plotting.py b/pandas/tools/plotting.py index 1433ce65b3021..ee27c81a27acb 100644 --- a/pandas/tools/plotting.py +++ b/pandas/tools/plotting.py @@ -1665,21 +1665,35 @@ def __init__(self, data, **kwargs): def _is_ts_plot(self): # this is slightly deceptive - return not self.x_compat and self.use_index and self._use_dynamic_x() - - def _use_dynamic_x(self): - from pandas.tseries.plotting import _use_dynamic_x - return _use_dynamic_x(self._get_ax(0), self.data) + from pandas.tseries.plotting import _use_dynamic_x, _get_freq + ax = self._get_ax(0) + freq, ax_freq = _get_freq(ax, self.data) + dynamic_x = _use_dynamic_x(ax, self.data) + return (not self.x_compat and self.use_index and + dynamic_x and freq is not None) def _make_plot(self): if self._is_ts_plot(): + print('tsplot-path!!') from pandas.tseries.plotting import _maybe_convert_index data = _maybe_convert_index(self._get_ax(0), self.data) + from pandas.tseries.plotting import _maybe_resample + for ax in self.axes: + # resample data and replot if required + kwds = self.kwds.copy() + data = _maybe_resample(data, ax, kwds) + x = data.index # dummy, not used plotf = self._ts_plot it = self._iter_data(data=data, keep_index=True) else: + print('xcompat-path!!') + from pandas.tseries.plotting import _replot_x_compat + for ax in self.axes: + # if ax holds _plot_data, replot them on the x_compat scale + _replot_x_compat(ax) + x = self._get_xticks(convert_period=True) plotf = self._plot it = self._iter_data() @@ -1723,24 +1737,15 @@ def _plot(cls, ax, x, y, style=None, column_num=None, @classmethod def _ts_plot(cls, ax, x, data, style=None, **kwds): - from pandas.tseries.plotting import (_maybe_resample, - _decorate_axes, - format_dateaxis) + from pandas.tseries.plotting import _maybe_resample, format_dateaxis # accept x to be consistent with normal plot func, # x is not passed to tsplot as it uses data.index as x coordinate # column_num must be in kwds for stacking purpose - freq, data = _maybe_resample(data, ax, kwds) - # Set ax with freq info - _decorate_axes(ax, freq, kwds) - # digging deeper - if hasattr(ax, 'left_ax'): - _decorate_axes(ax.left_ax, freq, kwds) - if hasattr(ax, 'right_ax'): - _decorate_axes(ax.right_ax, freq, kwds) + data = _maybe_resample(data, ax, kwds) ax._plot_data.append((data, cls._kind, kwds)) - lines = cls._plot(ax, data.index, data.values, style=style, **kwds) + # set date formatter, locators and rescale limits format_dateaxis(ax, ax.freq) return lines @@ -1790,14 +1795,9 @@ def _update_stacker(cls, ax, stacking_id, values): ax._stacker_neg_prior[stacking_id] += values def _post_plot_logic(self, ax, data): - condition = (not self._use_dynamic_x() and - data.index.is_all_dates and - not self.subplots or - (self.subplots and self.sharex)) - index_name = self._get_index_name() - if condition: + if not self._is_ts_plot(): # irregular TS rotated 30 deg. by default # probably a better place to check / set this. if not self._rot_set: diff --git a/pandas/tseries/plotting.py b/pandas/tseries/plotting.py index fe64af67af0ed..072c6aa3a4362 100644 --- a/pandas/tseries/plotting.py +++ b/pandas/tseries/plotting.py @@ -9,6 +9,8 @@ from matplotlib import pylab from pandas.tseries.period import Period +import numpy as np + from pandas.tseries.offsets import DateOffset import pandas.tseries.frequencies as frequencies from pandas.tseries.index import DatetimeIndex @@ -41,10 +43,7 @@ def tsplot(series, plotf, ax=None, **kwargs): import matplotlib.pyplot as plt ax = plt.gca() - freq, series = _maybe_resample(series, ax, kwargs) - - # Set ax with freq info - _decorate_axes(ax, freq, kwargs) + series = _maybe_resample(series, ax, kwargs) ax._plot_data.append((series, plotf, kwargs)) lines = plotf(ax, series.index._mpl_repr(), series.values, **kwargs) @@ -52,7 +51,6 @@ def tsplot(series, plotf, ax=None, **kwargs): format_dateaxis(ax, ax.freq) return lines - def _maybe_resample(series, ax, kwargs): # resample against axes freq if necessary freq, ax_freq = _get_freq(ax, series) @@ -75,11 +73,20 @@ def _maybe_resample(series, ax, kwargs): series = getattr(series.resample(ax_freq), how)().dropna() freq = ax_freq elif frequencies.is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq): - _upsample_others(ax, freq, kwargs) + _upsample_others(ax, freq) ax_freq = freq else: # pragma: no cover raise ValueError('Incompatible frequency conversion') - return freq, series + + # Set ax with freq info + _decorate_axes(ax, freq) + # digging deeper + if hasattr(ax, 'left_ax'): + _decorate_axes(ax.left_ax, freq) + elif hasattr(ax, 'right_ax'): + _decorate_axes(ax.right_ax, freq) + + return series def _is_sub(f1, f2): @@ -92,61 +99,84 @@ def _is_sup(f1, f2): (f2.startswith('W') and frequencies.is_superperiod(f1, 'D'))) -def _upsample_others(ax, freq, kwargs): - legend = ax.get_legend() - lines, labels = _replot_ax(ax, freq, kwargs) - _replot_ax(ax, freq, kwargs) +def _get_plot_func(plotf): + """ get actual function when plotf is specified with str """ + # for tsplot + if isinstance(plotf, compat.string_types): + from pandas.tools.plotting import _plot_klass + plotf = _plot_klass[plotf]._plot + return plotf + + +def _upsample_others(ax, freq): - other_ax = None + def _replot(ax): + data = getattr(ax, '_plot_data', None) + if data is None: + return + + # preserve legend + leg = ax.get_legend() + handles, labels = ax.get_legend_handles_labels() + + ax._plot_data = [] + ax.clear() + _decorate_axes(ax, freq) + + for series, plotf, kwds in data: + series = series.copy() + idx = series.index.asfreq(freq, how='s') + series.index = idx + ax._plot_data.append((series, plotf, kwds)) + + plotf = _get_plot_func(plotf) + plotf(ax, series.index._mpl_repr(), series.values, **kwds) + + + if leg is not None: + ax.legend(handles, labels, title=leg.get_title().get_text()) + + _replot(ax) if hasattr(ax, 'left_ax'): - other_ax = ax.left_ax - if hasattr(ax, 'right_ax'): - other_ax = ax.right_ax + _replot(ax.left_ax) + elif hasattr(ax, 'right_ax'): + _replot(ax.right_ax) - if other_ax is not None: - rlines, rlabels = _replot_ax(other_ax, freq, kwargs) - lines.extend(rlines) - labels.extend(rlabels) - if (legend is not None and kwargs.get('legend', True) and - len(lines) > 0): - title = legend.get_title().get_text() - if title == 'None': - title = None - ax.legend(lines, labels, loc='best', title=title) +def _replot_x_compat(ax): + def _replot(ax): + data = getattr(ax, '_plot_data', None) + if data is None: + return -def _replot_ax(ax, freq, kwargs): - data = getattr(ax, '_plot_data', None) + # preserve legend + leg = ax.get_legend() + handles, labels = ax.get_legend_handles_labels() - # clear current axes and data - ax._plot_data = [] - ax.clear() + ax._plot_data = None + ax.clear() - _decorate_axes(ax, freq, kwargs) + _decorate_axes(ax, None) - lines = [] - labels = [] - if data is not None: for series, plotf, kwds in data: - series = series.copy() - idx = series.index.asfreq(freq, how='S') + idx = series.index.to_timestamp(how='s') series.index = idx - ax._plot_data.append((series, plotf, kwds)) - # for tsplot - if isinstance(plotf, compat.string_types): - from pandas.tools.plotting import _plot_klass - plotf = _plot_klass[plotf]._plot + plotf = _get_plot_func(plotf) + plotf(ax, series.index._mpl_repr(), series, **kwds) - lines.append(plotf(ax, series.index._mpl_repr(), - series.values, **kwds)[0]) - labels.append(pprint_thing(series.name)) + if leg is not None: + ax.legend(handles, labels, title=leg.get_title().get_text()) - return lines, labels + _replot(ax) + if hasattr(ax, 'left_ax'): + _replot(ax.left_ax) + elif hasattr(ax, 'right_ax'): + _replot(ax.right_ax) -def _decorate_axes(ax, freq, kwargs): +def _decorate_axes(ax, freq): """Initialize axes for time-series plotting""" if not hasattr(ax, '_plot_data'): ax._plot_data = [] @@ -154,19 +184,27 @@ def _decorate_axes(ax, freq, kwargs): ax.freq = freq xaxis = ax.get_xaxis() xaxis.freq = freq - if not hasattr(ax, 'legendlabels'): - ax.legendlabels = [kwargs.get('label', None)] - else: - ax.legendlabels.append(kwargs.get('label', None)) ax.view_interval = None ax.date_axis_info = None -def _get_freq(ax, series): +def _get_index_freq(data): + freq = getattr(data.index, 'freq', None) + if freq is None: + freq = getattr(data.index, 'inferred_freq', None) + if freq == 'B': + weekdays = np.unique(data.index.dayofweek) + if (5 in weekdays) or (6 in weekdays): + freq = None + return freq + + +def _get_freq(ax, data): # get frequency from data - freq = getattr(series.index, 'freq', None) + freq = getattr(data.index, 'freq', None) + if freq is None: - freq = getattr(series.index, 'inferred_freq', None) + freq = getattr(data.index, 'inferred_freq', None) ax_freq = getattr(ax, 'freq', None) if ax_freq is None: @@ -175,17 +213,17 @@ def _get_freq(ax, series): elif hasattr(ax, 'right_ax'): ax_freq = getattr(ax.right_ax, 'freq', None) - # use axes freq if no data freq - if freq is None: - freq = ax_freq + if freq is not None: + # get the period frequency + if isinstance(freq, DateOffset): + freq = freq.rule_code + else: + freq = frequencies.get_base_alias(freq) - # get the period frequency - if isinstance(freq, DateOffset): - freq = freq.rule_code - else: - freq = frequencies.get_base_alias(freq) + if freq is None: + raise ValueError('Could not get frequency alias for plotting') + freq = frequencies.get_period_alias(freq) - freq = frequencies.get_period_alias(freq) return freq, ax_freq diff --git a/pandas/tseries/tests/test_plotting.py b/pandas/tseries/tests/test_plotting.py index 4a06a5500094a..baadabd49aea4 100644 --- a/pandas/tseries/tests/test_plotting.py +++ b/pandas/tseries/tests/test_plotting.py @@ -90,19 +90,16 @@ def test_nonnumeric_exclude(self): @slow def test_tsplot(self): - from pandas.tseries.plotting import tsplot import matplotlib.pyplot as plt ax = plt.gca() ts = tm.makeTimeSeries() - f = lambda *args, **kwds: tsplot(s, plt.Axes.plot, *args, **kwds) - for s in self.period_ser: - _check_plot_works(f, s.index.freq, ax=ax, series=s) + _check_plot_works(s.plot, ax=ax) for s in self.datetime_ser: - _check_plot_works(f, s.index.freq.rule_code, ax=ax, series=s) + _check_plot_works(s.plot, ax=ax) for s in self.period_ser: _check_plot_works(s.plot, ax=ax) @@ -640,27 +637,35 @@ def test_secondary_bar_frame(self): self.assertEqual(axes[2].get_yaxis().get_ticks_position(), 'right') def test_mixed_freq_regular_first(self): - import matplotlib.pyplot as plt # noqa s1 = tm.makeTimeSeries() s2 = s1[[0, 5, 10, 11, 12, 13, 14, 15]] + self.assertIsNone(s2.index.freq) - # it works! - s1.plot() - - ax2 = s2.plot(style='g') - lines = ax2.get_lines() - idx1 = PeriodIndex(lines[0].get_xdata()) - idx2 = PeriodIndex(lines[1].get_xdata()) + # the result has PeriodIndex axis + ax1 = s1.plot() + lines1 = ax1.get_lines() + idx1 = PeriodIndex(lines1[0].get_xdata()) self.assertTrue(idx1.equals(s1.index.to_period('B'))) - self.assertTrue(idx2.equals(s2.index.to_period('B'))) - left, right = ax2.get_xlim() + left, right = ax1.get_xlim() pidx = s1.index.to_period() self.assertEqual(left, pidx[0].ordinal) self.assertEqual(right, pidx[-1].ordinal) + # because s2 doesn't have freq, the result has x_compat axis + ax2 = s2.plot(style='g') + lines2 = ax2.get_lines() + + exp = s1.index.to_pydatetime() + tm.assert_numpy_array_equal(lines2[0].get_xdata(), exp) + tm.assert_numpy_array_equal(lines2[1].get_xdata(), + s2.index.to_pydatetime()) + left, right = ax2.get_xlim() + from matplotlib.dates import date2num + self.assertEqual(left, date2num(exp[0])) + self.assertEqual(right, date2num(exp[-1])) + @slow def test_mixed_freq_irregular_first(self): - import matplotlib.pyplot as plt # noqa s1 = tm.makeTimeSeries() s2 = s1[[0, 5, 10, 11, 12, 13, 14, 15]] s2.plot(style='g') @@ -672,23 +677,34 @@ def test_mixed_freq_irregular_first(self): x2 = lines[1].get_xdata() tm.assert_numpy_array_equal(x2, s1.index.asobject.values) - def test_mixed_freq_regular_first_df(self): + def test_aaamixed_freq_regular_first_df(self): # GH 9852 - import matplotlib.pyplot as plt # noqa s1 = tm.makeTimeSeries().to_frame() s2 = s1.iloc[[0, 5, 10, 11, 12, 13, 14, 15], :] - ax = s1.plot() - ax2 = s2.plot(style='g', ax=ax) - lines = ax2.get_lines() - idx1 = PeriodIndex(lines[0].get_xdata()) - idx2 = PeriodIndex(lines[1].get_xdata()) + + # the result has PeriodIndex axis + ax1 = s1.plot() + lines1 = ax1.get_lines() + idx1 = PeriodIndex(lines1[0].get_xdata()) self.assertTrue(idx1.equals(s1.index.to_period('B'))) - self.assertTrue(idx2.equals(s2.index.to_period('B'))) - left, right = ax2.get_xlim() + left, right = ax1.get_xlim() pidx = s1.index.to_period() self.assertEqual(left, pidx[0].ordinal) self.assertEqual(right, pidx[-1].ordinal) + # because s2 doesn't have freq, the result has x_compat axis + ax2 = s2.plot(style='g', ax=ax1) + lines2 = ax2.get_lines() + + exp = s1.index.to_pydatetime() + tm.assert_numpy_array_equal(lines2[0].get_xdata(), exp) + tm.assert_numpy_array_equal(lines2[1].get_xdata(), + s2.index.to_pydatetime()) + left, right = ax2.get_xlim() + from matplotlib.dates import date2num + self.assertEqual(left, date2num(exp[0])) + self.assertEqual(right, date2num(exp[-1])) + @slow def test_mixed_freq_irregular_first_df(self): # GH 9852