diff --git a/pandas/tseries/converter.py b/pandas/tseries/converter.py index 7e1c5f803008f..7dea69d353801 100644 --- a/pandas/tseries/converter.py +++ b/pandas/tseries/converter.py @@ -1,9 +1,13 @@ -from datetime import datetime +from datetime import datetime, timedelta import datetime as pydt import numpy as np +from dateutil.relativedelta import relativedelta + +import matplotlib import matplotlib.units as units import matplotlib.dates as dates + from matplotlib.ticker import Formatter, AutoLocator, Locator from matplotlib.transforms import nonsingular @@ -11,6 +15,7 @@ import pandas.core.common as com from pandas.core.index import Index +from pandas.tseries.index import date_range import pandas.tseries.tools as tools import pandas.tseries.frequencies as frequencies from pandas.tseries.frequencies import FreqGroup @@ -74,11 +79,14 @@ def __init__(self, locs): def __call__(self, x, pos=0): fmt = '%H:%M:%S' s = int(x) - us = int((x - s) * 1e6) + ms = int((x - s) * 1e3) + us = int((x - s) * 1e6 - ms) m, s = divmod(s, 60) h, m = divmod(m, 60) if us != 0: - fmt += '.%f' + fmt += '.%6f' + elif ms != 0: + fmt += '.%3f' return pydt.time(h, m, s, us).strftime(fmt) ### Period Conversion @@ -122,17 +130,7 @@ def _dt_to_float_ordinal(dt): preserving hours, minutes, seconds and microseconds. Return value is a :func:`float`. """ - - if hasattr(dt, 'tzinfo') and dt.tzinfo is not None: - delta = dt.tzinfo.utcoffset(dt) - if delta is not None: - dt -= delta - - base = float(dt.toordinal()) - if hasattr(dt, 'hour'): - base += (dt.hour/HOURS_PER_DAY + dt.minute/MINUTES_PER_DAY + - dt.second/SECONDS_PER_DAY + dt.microsecond/MUSECONDS_PER_DAY - ) + base = dates.date2num(dt) return base ### Datetime Conversion @@ -160,6 +158,209 @@ def try_parse(values): return [try_parse(x) for x in values] return values + @staticmethod + def axisinfo(unit, axis): + """ + Return the :class:`~matplotlib.units.AxisInfo` for *unit*. + + *unit* is a tzinfo instance or None. + The *axis* argument is required but not used. + """ + tz = unit + + majloc = PandasAutoDateLocator(tz=tz) + majfmt = PandasAutoDateFormatter(majloc, tz=tz) + datemin = pydt.date(2000, 1, 1) + datemax = pydt.date(2010, 1, 1) + + return units.AxisInfo( majloc=majloc, majfmt=majfmt, label='', + default_limits=(datemin, datemax)) + + +class PandasAutoDateFormatter(dates.AutoDateFormatter): + + def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'): + dates.AutoDateFormatter.__init__(self, locator, tz, defaultfmt) + # matplotlib.dates._UTC has no _utcoffset called by pandas + if self._tz is dates.UTC: + self._tz._utcoffset = self._tz.utcoffset(None) + self.scaled = { + 365.0 : '%Y', + 30. : '%b %Y', + 1.0 : '%b %d %Y', + 1. / 24. : '%H:%M:%S', + 1. / 24. / 3600. / 1000. : '%H:%M:%S.%f' + } + + def _get_fmt(self, x): + + scale = float( self._locator._get_unit() ) + + fmt = self.defaultfmt + + for k in sorted(self.scaled): + if k >= scale: + fmt = self.scaled[k] + break + + return fmt + + def __call__(self, x, pos=0): + fmt = self._get_fmt(x) + self._formatter = dates.DateFormatter(fmt, self._tz) + return self._formatter(x, pos) + +class PandasAutoDateLocator(dates.AutoDateLocator): + + def get_locator(self, dmin, dmax): + 'Pick the best locator based on a distance.' + delta = relativedelta(dmax, dmin) + + num_days = ((delta.years * 12.0) + delta.months * 31.0) + delta.days + num_sec = (delta.hours * 60.0 + delta.minutes) * 60.0 + delta.seconds + tot_sec = num_days * 86400. + num_sec + + if tot_sec < self.minticks: + self._freq = -1 + locator = MilliSecondLocator(self.tz) + locator.set_axis(self.axis) + + locator.set_view_interval(*self.axis.get_view_interval()) + locator.set_data_interval(*self.axis.get_data_interval()) + return locator + + return dates.AutoDateLocator.get_locator(self, dmin, dmax) + + def _get_unit(self): + return MilliSecondLocator.get_unit_generic(self._freq) + +class MilliSecondLocator(dates.DateLocator): + + UNIT = 1. / (24 * 3600 * 1000) + + def __init__(self, tz): + dates.DateLocator.__init__(self, tz) + self._interval = 1. + + def _get_unit(self): + return self.get_unit_generic(-1) + + @staticmethod + def get_unit_generic(freq): + unit = dates.RRuleLocator.get_unit_generic(freq) + if unit < 0: + return MilliSecondLocator.UNIT + return unit + + def __call__(self): + # if no data have been set, this will tank with a ValueError + try: dmin, dmax = self.viewlim_to_dt() + except ValueError: return [] + + if dmin>dmax: + dmax, dmin = dmin, dmax + delta = relativedelta(dmax, dmin) + + # We need to cap at the endpoints of valid datetime + try: + start = dmin - delta + except ValueError: + start = _from_ordinal( 1.0 ) + + try: + stop = dmax + delta + except ValueError: + # The magic number! + stop = _from_ordinal( 3652059.9999999 ) + + nmax, nmin = dates.date2num((dmax, dmin)) + + num = (nmax - nmin) * 86400 * 1000 + max_millis_ticks = 6 + for interval in [1, 10, 50, 100, 200, 500]: + if num <= interval * (max_millis_ticks - 1): + self._interval = interval + break + else: + # We went through the whole loop without breaking, default to 1 + self._interval = 1000. + + estimate = (nmax - nmin) / (self._get_unit() * self._get_interval()) + + if estimate > self.MAXTICKS * 2: + raise RuntimeError(('MillisecondLocator estimated to generate %d ' + 'ticks from %s to %s: exceeds Locator.MAXTICKS' + '* 2 (%d) ') % + (estimate, dmin, dmax, self.MAXTICKS * 2)) + + freq = '%dL' % self._get_interval() + tz = self.tz.tzname(None) + st = _from_ordinal(dates.date2num(dmin)) # strip tz + ed = _from_ordinal(dates.date2num(dmax)) + all_dates = date_range(start=st, end=ed, freq=freq, tz=tz).asobject + + try: + if len(all_dates) > 0: + locs = self.raise_if_exceeds(dates.date2num(all_dates)) + return locs + except Exception, e: + pass + + lims = dates.date2num([dmin, dmax]) + return lims + + def _get_interval(self): + return self._interval + + def autoscale(self): + """ + Set the view limits to include the data range. + """ + dmin, dmax = self.datalim_to_dt() + if dmin>dmax: + dmax, dmin = dmin, dmax + + delta = relativedelta(dmax, dmin) + + # We need to cap at the endpoints of valid datetime + try: + start = dmin - delta + except ValueError: + start = _from_ordinal(1.0) + + try: + stop = dmax + delta + except ValueError: + # The magic number! + stop = _from_ordinal( 3652059.9999999 ) + + dmin, dmax = self.datalim_to_dt() + + vmin = dates.date2num(dmin) + vmax = dates.date2num(dmax) + + return self.nonsingular(vmin, vmax) + + +def _from_ordinal(x, tz=None): + ix = int(x) + dt = datetime.fromordinal(ix) + remainder = float(x) - ix + hour, remainder = divmod(24*remainder, 1) + minute, remainder = divmod(60*remainder, 1) + second, remainder = divmod(60*remainder, 1) + microsecond = int(1e6*remainder) + if microsecond<10: microsecond=0 # compensate for rounding errors + dt = datetime(dt.year, dt.month, dt.day, int(hour), int(minute), + int(second), microsecond) + if tz is not None: + dt = dt.astimezone(tz) + + if microsecond > 999990: # compensate for rounding errors + dt += timedelta(microseconds = 1e6 - microsecond) + + return dt + ### Fixed frequency dynamic tick locators and formatters ##### ------------------------------------------------------------------------- @@ -717,4 +918,3 @@ def __call__(self, x, pos=0): fmt = self.formatdict.pop(x, '') return Period(ordinal=int(x), freq=self.freq).strftime(fmt) - diff --git a/pandas/tseries/tests/test_plotting.py b/pandas/tseries/tests/test_plotting.py index b2f49beeb6f1b..159803dedb45c 100644 --- a/pandas/tseries/tests/test_plotting.py +++ b/pandas/tseries/tests/test_plotting.py @@ -16,6 +16,7 @@ from pandas.tseries.resample import DatetimeIndex, TimeGrouper import pandas.tseries.offsets as offsets import pandas.tseries.frequencies as frequencies +import pandas.tseries.converter as conv from pandas.util.testing import assert_series_equal, assert_almost_equal import pandas.util.testing as tm @@ -154,6 +155,27 @@ def test_plot_multiple_inferred_freq(self): ser = Series(np.random.randn(len(dr)), dr) _check_plot_works(ser.plot) + @slow + def test_uhf(self): + import matplotlib.pyplot as plt + fig = plt.gcf() + plt.clf() + fig.add_subplot(111) + + idx = date_range('2012-6-22 21:59:51.960928', freq='L', periods=500) + df = DataFrame(np.random.randn(len(idx), 2), idx) + + ax = df.plot() + axis = ax.get_xaxis() + + tlocs = axis.get_ticklocs() + tlabels = axis.get_ticklabels() + for loc, label in zip(tlocs, tlabels): + xp = conv._from_ordinal(loc).strftime('%H:%M:%S.%f') + rs = str(label.get_text()) + if len(rs) != 0: + self.assert_(xp == rs) + @slow def test_irreg_hf(self): import matplotlib.pyplot as plt