|
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 | + 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 | + |
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 |
| - |
|
0 commit comments