Skip to content

Commit 367e981

Browse files
Chang Shewesm
Chang She
authored andcommitted
BUG: new converters for sub-second plotting #1599
1 parent 9a0e52a commit 367e981

File tree

2 files changed

+237
-15
lines changed

2 files changed

+237
-15
lines changed

pandas/tseries/converter.py

+215-15
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
from datetime import datetime
1+
from datetime import datetime, timedelta
22
import datetime as pydt
33
import numpy as np
44

5+
from dateutil.relativedelta import relativedelta
6+
7+
import matplotlib
58
import matplotlib.units as units
69
import matplotlib.dates as dates
10+
711
from matplotlib.ticker import Formatter, AutoLocator, Locator
812
from matplotlib.transforms import nonsingular
913

1014
import pandas.lib as lib
1115
import pandas.core.common as com
1216
from pandas.core.index import Index
1317

18+
from pandas.tseries.index import date_range
1419
import pandas.tseries.tools as tools
1520
import pandas.tseries.frequencies as frequencies
1621
from pandas.tseries.frequencies import FreqGroup
@@ -74,11 +79,14 @@ def __init__(self, locs):
7479
def __call__(self, x, pos=0):
7580
fmt = '%H:%M:%S'
7681
s = int(x)
77-
us = int((x - s) * 1e6)
82+
ms = int((x - s) * 1e3)
83+
us = int((x - s) * 1e6 - ms)
7884
m, s = divmod(s, 60)
7985
h, m = divmod(m, 60)
8086
if us != 0:
81-
fmt += '.%f'
87+
fmt += '.%6f'
88+
elif ms != 0:
89+
fmt += '.%3f'
8290
return pydt.time(h, m, s, us).strftime(fmt)
8391

8492
### Period Conversion
@@ -122,17 +130,7 @@ def _dt_to_float_ordinal(dt):
122130
preserving hours, minutes, seconds and microseconds. Return value
123131
is a :func:`float`.
124132
"""
125-
126-
if hasattr(dt, 'tzinfo') and dt.tzinfo is not None:
127-
delta = dt.tzinfo.utcoffset(dt)
128-
if delta is not None:
129-
dt -= delta
130-
131-
base = float(dt.toordinal())
132-
if hasattr(dt, 'hour'):
133-
base += (dt.hour/HOURS_PER_DAY + dt.minute/MINUTES_PER_DAY +
134-
dt.second/SECONDS_PER_DAY + dt.microsecond/MUSECONDS_PER_DAY
135-
)
133+
base = dates.date2num(dt)
136134
return base
137135

138136
### Datetime Conversion
@@ -160,6 +158,209 @@ def try_parse(values):
160158
return [try_parse(x) for x in values]
161159
return values
162160

161+
@staticmethod
162+
def axisinfo(unit, axis):
163+
"""
164+
Return the :class:`~matplotlib.units.AxisInfo` for *unit*.
165+
166+
*unit* is a tzinfo instance or None.
167+
The *axis* argument is required but not used.
168+
"""
169+
tz = unit
170+
171+
majloc = PandasAutoDateLocator(tz=tz)
172+
majfmt = PandasAutoDateFormatter(majloc, tz=tz)
173+
datemin = pydt.date(2000, 1, 1)
174+
datemax = pydt.date(2010, 1, 1)
175+
176+
return units.AxisInfo( majloc=majloc, majfmt=majfmt, label='',
177+
default_limits=(datemin, datemax))
178+
179+
180+
class PandasAutoDateFormatter(dates.AutoDateFormatter):
181+
182+
def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'):
183+
dates.AutoDateFormatter.__init__(self, locator, tz, defaultfmt)
184+
# matplotlib.dates._UTC has no _utcoffset called by pandas
185+
if self._tz is dates.UTC:
186+
self._tz._utcoffset = self._tz.utcoffset(None)
187+
self.scaled = {
188+
365.0 : '%Y',
189+
30. : '%b %Y',
190+
1.0 : '%b %d %Y',
191+
1. / 24. : '%H:%M:%S',
192+
1. / 24. / 3600. / 1000. : '%H:%M:%S.%f'
193+
}
194+
195+
def _get_fmt(self, x):
196+
197+
scale = float( self._locator._get_unit() )
198+
199+
fmt = self.defaultfmt
200+
201+
for k in sorted(self.scaled):
202+
if k >= scale:
203+
fmt = self.scaled[k]
204+
break
205+
206+
return fmt
207+
208+
def __call__(self, x, pos=0):
209+
fmt = self._get_fmt(x)
210+
self._formatter = dates.DateFormatter(fmt, self._tz)
211+
return self._formatter(x, pos)
212+
213+
class PandasAutoDateLocator(dates.AutoDateLocator):
214+
215+
def get_locator(self, dmin, dmax):
216+
'Pick the best locator based on a distance.'
217+
delta = relativedelta(dmax, dmin)
218+
219+
num_days = ((delta.years * 12.0) + delta.months * 31.0) + delta.days
220+
num_sec = (delta.hours * 60.0 + delta.minutes) * 60.0 + delta.seconds
221+
tot_sec = num_days * 86400. + num_sec
222+
223+
if tot_sec < self.minticks:
224+
self._freq = -1
225+
locator = MilliSecondLocator(self.tz)
226+
locator.set_axis(self.axis)
227+
228+
locator.set_view_interval(*self.axis.get_view_interval())
229+
locator.set_data_interval(*self.axis.get_data_interval())
230+
return locator
231+
232+
return dates.AutoDateLocator.get_locator(self, dmin, dmax)
233+
234+
def _get_unit(self):
235+
return MilliSecondLocator.get_unit_generic(self._freq)
236+
237+
class MilliSecondLocator(dates.DateLocator):
238+
239+
UNIT = 1. / (24 * 3600 * 1000)
240+
241+
def __init__(self, tz):
242+
dates.DateLocator.__init__(self, tz)
243+
self._interval = 1.
244+
245+
def _get_unit(self):
246+
return self.get_unit_generic(-1)
247+
248+
@staticmethod
249+
def get_unit_generic(freq):
250+
unit = dates.RRuleLocator.get_unit_generic(freq)
251+
if unit < 0:
252+
return MilliSecondLocator.UNIT
253+
return unit
254+
255+
def __call__(self):
256+
# if no data have been set, this will tank with a ValueError
257+
try: dmin, dmax = self.viewlim_to_dt()
258+
except ValueError: return []
259+
260+
if dmin>dmax:
261+
dmax, dmin = dmin, dmax
262+
delta = relativedelta(dmax, dmin)
263+
264+
# We need to cap at the endpoints of valid datetime
265+
try:
266+
start = dmin - delta
267+
except ValueError:
268+
start = _from_ordinal( 1.0 )
269+
270+
try:
271+
stop = dmax + delta
272+
except ValueError:
273+
# The magic number!
274+
stop = _from_ordinal( 3652059.9999999 )
275+
276+
nmax, nmin = dates.date2num((dmax, dmin))
277+
278+
num = (nmax - nmin) * 86400 * 1000
279+
max_millis_ticks = 6
280+
for interval in [1, 10, 50, 100, 200, 500]:
281+
if num <= interval * (max_millis_ticks - 1):
282+
self._interval = interval
283+
break
284+
else:
285+
# We went through the whole loop without breaking, default to 1
286+
self._interval = 1000.
287+
288+
estimate = (nmax - nmin) / (self._get_unit() * self._get_interval())
289+
290+
if estimate > self.MAXTICKS * 2:
291+
raise RuntimeError(('MillisecondLocator estimated to generate %d '
292+
'ticks from %s to %s: exceeds Locator.MAXTICKS'
293+
'* 2 (%d) ') %
294+
(estimate, dmin, dmax, self.MAXTICKS * 2))
295+
296+
freq = '%dL' % self._get_interval()
297+
tz = self.tz.tzname(None)
298+
st = _from_ordinal(dates.date2num(dmin)) # strip tz
299+
ed = _from_ordinal(dates.date2num(dmax))
300+
all_dates = date_range(start=st, end=ed, freq=freq, tz=tz).asobject
301+
302+
try:
303+
if len(all_dates) > 0:
304+
locs = self.raise_if_exceeds(dates.date2num(all_dates))
305+
return locs
306+
except Exception, e:
307+
pass
308+
309+
lims = dates.date2num([dmin, dmax])
310+
return lims
311+
312+
def _get_interval(self):
313+
return self._interval
314+
315+
def autoscale(self):
316+
"""
317+
Set the view limits to include the data range.
318+
"""
319+
dmin, dmax = self.datalim_to_dt()
320+
if dmin>dmax:
321+
dmax, dmin = dmin, dmax
322+
323+
delta = relativedelta(dmax, dmin)
324+
325+
# We need to cap at the endpoints of valid datetime
326+
try:
327+
start = dmin - delta
328+
except ValueError:
329+
start = _from_ordinal(1.0)
330+
331+
try:
332+
stop = dmax + delta
333+
except ValueError:
334+
# The magic number!
335+
stop = _from_ordinal( 3652059.9999999 )
336+
337+
dmin, dmax = self.datalim_to_dt()
338+
339+
vmin = dates.date2num(dmin)
340+
vmax = dates.date2num(dmax)
341+
342+
return self.nonsingular(vmin, vmax)
343+
344+
345+
def _from_ordinal(x, tz=None):
346+
ix = int(x)
347+
dt = datetime.fromordinal(ix)
348+
remainder = float(x) - ix
349+
hour, remainder = divmod(24*remainder, 1)
350+
minute, remainder = divmod(60*remainder, 1)
351+
second, remainder = divmod(60*remainder, 1)
352+
microsecond = int(1e6*remainder)
353+
if microsecond<10: microsecond=0 # compensate for rounding errors
354+
dt = datetime(dt.year, dt.month, dt.day, int(hour), int(minute),
355+
int(second), microsecond)
356+
if tz is not None:
357+
dt = dt.astimezone(tz)
358+
359+
if microsecond > 999990: # compensate for rounding errors
360+
dt += timedelta(microseconds = 1e6 - microsecond)
361+
362+
return dt
363+
163364
### Fixed frequency dynamic tick locators and formatters
164365

165366
##### -------------------------------------------------------------------------
@@ -717,4 +918,3 @@ def __call__(self, x, pos=0):
717918
fmt = self.formatdict.pop(x, '')
718919
return Period(ordinal=int(x), freq=self.freq).strftime(fmt)
719920

720-

pandas/tseries/tests/test_plotting.py

+22
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pandas.tseries.resample import DatetimeIndex, TimeGrouper
1717
import pandas.tseries.offsets as offsets
1818
import pandas.tseries.frequencies as frequencies
19+
import pandas.tseries.converter as conv
1920

2021
from pandas.util.testing import assert_series_equal, assert_almost_equal
2122
import pandas.util.testing as tm
@@ -154,6 +155,27 @@ def test_plot_multiple_inferred_freq(self):
154155
ser = Series(np.random.randn(len(dr)), dr)
155156
_check_plot_works(ser.plot)
156157

158+
@slow
159+
def test_uhf(self):
160+
import matplotlib.pyplot as plt
161+
fig = plt.gcf()
162+
plt.clf()
163+
fig.add_subplot(111)
164+
165+
idx = date_range('2012-6-22 21:59:51.960928', freq='L', periods=500)
166+
df = DataFrame(np.random.randn(len(idx), 2), idx)
167+
168+
ax = df.plot()
169+
axis = ax.get_xaxis()
170+
171+
tlocs = axis.get_ticklocs()
172+
tlabels = axis.get_ticklabels()
173+
for loc, label in zip(tlocs, tlabels):
174+
xp = conv._from_ordinal(loc).strftime('%H:%M:%S.%f')
175+
rs = str(label.get_text())
176+
if len(rs) != 0:
177+
self.assert_(xp == rs)
178+
157179
@slow
158180
def test_irreg_hf(self):
159181
import matplotlib.pyplot as plt

0 commit comments

Comments
 (0)