BUG: new converters for sub-second plotting #1599 · pandas-dev/pandas@d0d15b0 (original) (raw)
1
``
`-
from datetime import datetime
`
``
1
`+
from datetime import datetime, timedelta
`
2
2
`import datetime as pydt
`
3
3
`import numpy as np
`
4
4
``
``
5
`+
from dateutil.relativedelta import relativedelta
`
``
6
+
``
7
`+
import matplotlib
`
5
8
`import matplotlib.units as units
`
6
9
`import matplotlib.dates as dates
`
``
10
+
7
11
`from matplotlib.ticker import Formatter, AutoLocator, Locator
`
8
12
`from matplotlib.transforms import nonsingular
`
9
13
``
10
14
`import pandas.lib as lib
`
11
15
`import pandas.core.common as com
`
12
16
`from pandas.core.index import Index
`
13
17
``
``
18
`+
from pandas.tseries.index import date_range
`
14
19
`import pandas.tseries.tools as tools
`
15
20
`import pandas.tseries.frequencies as frequencies
`
16
21
`from pandas.tseries.frequencies import FreqGroup
`
`@@ -74,11 +79,14 @@ def init(self, locs):
`
74
79
`def call(self, x, pos=0):
`
75
80
`fmt = '%H:%M:%S'
`
76
81
`s = int(x)
`
77
``
`-
us = int((x - s) * 1e6)
`
``
82
`+
ms = int((x - s) * 1e3)
`
``
83
`+
us = int((x - s) * 1e6 - ms)
`
78
84
`m, s = divmod(s, 60)
`
79
85
`h, m = divmod(m, 60)
`
80
86
`if us != 0:
`
81
``
`-
fmt += '.%f'
`
``
87
`+
fmt += '.%6f'
`
``
88
`+
elif ms != 0:
`
``
89
`+
fmt += '.%3f'
`
82
90
`return pydt.time(h, m, s, us).strftime(fmt)
`
83
91
``
84
92
`### Period Conversion
`
`@@ -122,17 +130,7 @@ def _dt_to_float_ordinal(dt):
`
122
130
` preserving hours, minutes, seconds and microseconds. Return value
`
123
131
`` is a :func:float
.
``
124
132
` """
`
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)
`
136
134
`return base
`
137
135
``
138
136
`### Datetime Conversion
`
`@@ -160,6 +158,209 @@ def try_parse(values):
`
160
158
`return [try_parse(x) for x in values]
`
161
159
`return values
`
162
160
``
``
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
`+
- : '%b %Y',
`
``
190
`+
1.0 : '%b %d %Y',
`
``
191
`+
- / 24. : '%H:%M:%S',
`
``
192
`+
- / 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
+
163
364
`### Fixed frequency dynamic tick locators and formatters
`
164
365
``
165
366
`##### -------------------------------------------------------------------------
`
`@@ -717,4 +918,3 @@ def call(self, x, pos=0):
`
717
918
`fmt = self.formatdict.pop(x, '')
`
718
919
`return Period(ordinal=int(x), freq=self.freq).strftime(fmt)
`
719
920
``
720
``
-