Skip to content

Commit

Permalink
BUG: new converters for sub-second plotting #1599
Browse files Browse the repository at this point in the history
  • Loading branch information
Chang She authored and wesm committed Jul 13, 2012
1 parent 9a0e52a commit 367e981
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 15 deletions.
230 changes: 215 additions & 15 deletions pandas/tseries/converter.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
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

import pandas.lib as lib
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

##### -------------------------------------------------------------------------
Expand Down Expand Up @@ -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)


22 changes: 22 additions & 0 deletions pandas/tseries/tests/test_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 367e981

Please sign in to comment.