_converter.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  1. import datetime as pydt
  2. from datetime import datetime, timedelta
  3. import warnings
  4. from dateutil.relativedelta import relativedelta
  5. import matplotlib.dates as dates
  6. from matplotlib.ticker import AutoLocator, Formatter, Locator
  7. from matplotlib.transforms import nonsingular
  8. import matplotlib.units as units
  9. import numpy as np
  10. from pandas._libs import lib, tslibs
  11. from pandas._libs.tslibs import resolution
  12. from pandas._libs.tslibs.frequencies import FreqGroup, get_freq
  13. import pandas.compat as compat
  14. from pandas.compat import lrange
  15. from pandas.core.dtypes.common import (
  16. is_datetime64_ns_dtype, is_float, is_float_dtype, is_integer,
  17. is_integer_dtype, is_nested_list_like)
  18. from pandas.core.dtypes.generic import ABCSeries
  19. import pandas.core.common as com
  20. from pandas.core.index import Index
  21. from pandas.core.indexes.datetimes import date_range
  22. from pandas.core.indexes.period import Period, PeriodIndex, period_range
  23. import pandas.core.tools.datetimes as tools
  24. # constants
  25. HOURS_PER_DAY = 24.
  26. MIN_PER_HOUR = 60.
  27. SEC_PER_MIN = 60.
  28. SEC_PER_HOUR = SEC_PER_MIN * MIN_PER_HOUR
  29. SEC_PER_DAY = SEC_PER_HOUR * HOURS_PER_DAY
  30. MUSEC_PER_DAY = 1e6 * SEC_PER_DAY
  31. _WARN = True # Global for whether pandas has registered the units explicitly
  32. _mpl_units = {} # Cache for units overwritten by us
  33. def get_pairs():
  34. pairs = [
  35. (tslibs.Timestamp, DatetimeConverter),
  36. (Period, PeriodConverter),
  37. (pydt.datetime, DatetimeConverter),
  38. (pydt.date, DatetimeConverter),
  39. (pydt.time, TimeConverter),
  40. (np.datetime64, DatetimeConverter),
  41. ]
  42. return pairs
  43. def register(explicit=True):
  44. """
  45. Register Pandas Formatters and Converters with matplotlib
  46. This function modifies the global ``matplotlib.units.registry``
  47. dictionary. Pandas adds custom converters for
  48. * pd.Timestamp
  49. * pd.Period
  50. * np.datetime64
  51. * datetime.datetime
  52. * datetime.date
  53. * datetime.time
  54. See Also
  55. --------
  56. deregister_matplotlib_converter
  57. """
  58. # Renamed in pandas.plotting.__init__
  59. global _WARN
  60. if explicit:
  61. _WARN = False
  62. pairs = get_pairs()
  63. for type_, cls in pairs:
  64. converter = cls()
  65. if type_ in units.registry:
  66. previous = units.registry[type_]
  67. _mpl_units[type_] = previous
  68. units.registry[type_] = converter
  69. def deregister():
  70. """
  71. Remove pandas' formatters and converters
  72. Removes the custom converters added by :func:`register`. This
  73. attempts to set the state of the registry back to the state before
  74. pandas registered its own units. Converters for pandas' own types like
  75. Timestamp and Period are removed completely. Converters for types
  76. pandas overwrites, like ``datetime.datetime``, are restored to their
  77. original value.
  78. See Also
  79. --------
  80. deregister_matplotlib_converters
  81. """
  82. # Renamed in pandas.plotting.__init__
  83. for type_, cls in get_pairs():
  84. # We use type to catch our classes directly, no inheritance
  85. if type(units.registry.get(type_)) is cls:
  86. units.registry.pop(type_)
  87. # restore the old keys
  88. for unit, formatter in _mpl_units.items():
  89. if type(formatter) not in {DatetimeConverter, PeriodConverter,
  90. TimeConverter}:
  91. # make it idempotent by excluding ours.
  92. units.registry[unit] = formatter
  93. def _check_implicitly_registered():
  94. global _WARN
  95. if _WARN:
  96. msg = ("Using an implicitly registered datetime converter for a "
  97. "matplotlib plotting method. The converter was registered "
  98. "by pandas on import. Future versions of pandas will require "
  99. "you to explicitly register matplotlib converters.\n\n"
  100. "To register the converters:\n\t"
  101. ">>> from pandas.plotting import register_matplotlib_converters"
  102. "\n\t"
  103. ">>> register_matplotlib_converters()")
  104. warnings.warn(msg, FutureWarning)
  105. _WARN = False
  106. def _to_ordinalf(tm):
  107. tot_sec = (tm.hour * 3600 + tm.minute * 60 + tm.second +
  108. float(tm.microsecond / 1e6))
  109. return tot_sec
  110. def time2num(d):
  111. if isinstance(d, compat.string_types):
  112. parsed = tools.to_datetime(d)
  113. if not isinstance(parsed, datetime):
  114. raise ValueError('Could not parse time {d}'.format(d=d))
  115. return _to_ordinalf(parsed.time())
  116. if isinstance(d, pydt.time):
  117. return _to_ordinalf(d)
  118. return d
  119. class TimeConverter(units.ConversionInterface):
  120. @staticmethod
  121. def convert(value, unit, axis):
  122. valid_types = (str, pydt.time)
  123. if (isinstance(value, valid_types) or is_integer(value) or
  124. is_float(value)):
  125. return time2num(value)
  126. if isinstance(value, Index):
  127. return value.map(time2num)
  128. if isinstance(value, (list, tuple, np.ndarray, Index)):
  129. return [time2num(x) for x in value]
  130. return value
  131. @staticmethod
  132. def axisinfo(unit, axis):
  133. if unit != 'time':
  134. return None
  135. majloc = AutoLocator()
  136. majfmt = TimeFormatter(majloc)
  137. return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='time')
  138. @staticmethod
  139. def default_units(x, axis):
  140. return 'time'
  141. # time formatter
  142. class TimeFormatter(Formatter):
  143. def __init__(self, locs):
  144. self.locs = locs
  145. def __call__(self, x, pos=0):
  146. """
  147. Return the time of day as a formatted string.
  148. Parameters
  149. ----------
  150. x : float
  151. The time of day specified as seconds since 00:00 (midnight),
  152. with up to microsecond precision.
  153. pos
  154. Unused
  155. Returns
  156. -------
  157. str
  158. A string in HH:MM:SS.mmmuuu format. Microseconds,
  159. milliseconds and seconds are only displayed if non-zero.
  160. """
  161. fmt = '%H:%M:%S.%f'
  162. s = int(x)
  163. msus = int(round((x - s) * 1e6))
  164. ms = msus // 1000
  165. us = msus % 1000
  166. m, s = divmod(s, 60)
  167. h, m = divmod(m, 60)
  168. _, h = divmod(h, 24)
  169. if us != 0:
  170. return pydt.time(h, m, s, msus).strftime(fmt)
  171. elif ms != 0:
  172. return pydt.time(h, m, s, msus).strftime(fmt)[:-3]
  173. elif s != 0:
  174. return pydt.time(h, m, s).strftime('%H:%M:%S')
  175. return pydt.time(h, m).strftime('%H:%M')
  176. # Period Conversion
  177. class PeriodConverter(dates.DateConverter):
  178. @staticmethod
  179. def convert(values, units, axis):
  180. if is_nested_list_like(values):
  181. values = [PeriodConverter._convert_1d(v, units, axis)
  182. for v in values]
  183. else:
  184. values = PeriodConverter._convert_1d(values, units, axis)
  185. return values
  186. @staticmethod
  187. def _convert_1d(values, units, axis):
  188. if not hasattr(axis, 'freq'):
  189. raise TypeError('Axis must have `freq` set to convert to Periods')
  190. valid_types = (compat.string_types, datetime,
  191. Period, pydt.date, pydt.time, np.datetime64)
  192. if (isinstance(values, valid_types) or is_integer(values) or
  193. is_float(values)):
  194. return get_datevalue(values, axis.freq)
  195. elif isinstance(values, PeriodIndex):
  196. return values.asfreq(axis.freq)._ndarray_values
  197. elif isinstance(values, Index):
  198. return values.map(lambda x: get_datevalue(x, axis.freq))
  199. elif lib.infer_dtype(values, skipna=False) == 'period':
  200. # https://github.com/pandas-dev/pandas/issues/24304
  201. # convert ndarray[period] -> PeriodIndex
  202. return PeriodIndex(values, freq=axis.freq)._ndarray_values
  203. elif isinstance(values, (list, tuple, np.ndarray, Index)):
  204. return [get_datevalue(x, axis.freq) for x in values]
  205. return values
  206. def get_datevalue(date, freq):
  207. if isinstance(date, Period):
  208. return date.asfreq(freq).ordinal
  209. elif isinstance(date, (compat.string_types, datetime,
  210. pydt.date, pydt.time, np.datetime64)):
  211. return Period(date, freq).ordinal
  212. elif (is_integer(date) or is_float(date) or
  213. (isinstance(date, (np.ndarray, Index)) and (date.size == 1))):
  214. return date
  215. elif date is None:
  216. return None
  217. raise ValueError("Unrecognizable date '{date}'".format(date=date))
  218. def _dt_to_float_ordinal(dt):
  219. """
  220. Convert :mod:`datetime` to the Gregorian date as UTC float days,
  221. preserving hours, minutes, seconds and microseconds. Return value
  222. is a :func:`float`.
  223. """
  224. if (isinstance(dt, (np.ndarray, Index, ABCSeries)
  225. ) and is_datetime64_ns_dtype(dt)):
  226. base = dates.epoch2num(dt.asi8 / 1.0E9)
  227. else:
  228. base = dates.date2num(dt)
  229. return base
  230. # Datetime Conversion
  231. class DatetimeConverter(dates.DateConverter):
  232. @staticmethod
  233. def convert(values, unit, axis):
  234. # values might be a 1-d array, or a list-like of arrays.
  235. _check_implicitly_registered()
  236. if is_nested_list_like(values):
  237. values = [DatetimeConverter._convert_1d(v, unit, axis)
  238. for v in values]
  239. else:
  240. values = DatetimeConverter._convert_1d(values, unit, axis)
  241. return values
  242. @staticmethod
  243. def _convert_1d(values, unit, axis):
  244. def try_parse(values):
  245. try:
  246. return _dt_to_float_ordinal(tools.to_datetime(values))
  247. except Exception:
  248. return values
  249. if isinstance(values, (datetime, pydt.date)):
  250. return _dt_to_float_ordinal(values)
  251. elif isinstance(values, np.datetime64):
  252. return _dt_to_float_ordinal(tslibs.Timestamp(values))
  253. elif isinstance(values, pydt.time):
  254. return dates.date2num(values)
  255. elif (is_integer(values) or is_float(values)):
  256. return values
  257. elif isinstance(values, compat.string_types):
  258. return try_parse(values)
  259. elif isinstance(values, (list, tuple, np.ndarray, Index, ABCSeries)):
  260. if isinstance(values, ABCSeries):
  261. # https://github.com/matplotlib/matplotlib/issues/11391
  262. # Series was skipped. Convert to DatetimeIndex to get asi8
  263. values = Index(values)
  264. if isinstance(values, Index):
  265. values = values.values
  266. if not isinstance(values, np.ndarray):
  267. values = com.asarray_tuplesafe(values)
  268. if is_integer_dtype(values) or is_float_dtype(values):
  269. return values
  270. try:
  271. values = tools.to_datetime(values)
  272. if isinstance(values, Index):
  273. values = _dt_to_float_ordinal(values)
  274. else:
  275. values = [_dt_to_float_ordinal(x) for x in values]
  276. except Exception:
  277. values = _dt_to_float_ordinal(values)
  278. return values
  279. @staticmethod
  280. def axisinfo(unit, axis):
  281. """
  282. Return the :class:`~matplotlib.units.AxisInfo` for *unit*.
  283. *unit* is a tzinfo instance or None.
  284. The *axis* argument is required but not used.
  285. """
  286. tz = unit
  287. majloc = PandasAutoDateLocator(tz=tz)
  288. majfmt = PandasAutoDateFormatter(majloc, tz=tz)
  289. datemin = pydt.date(2000, 1, 1)
  290. datemax = pydt.date(2010, 1, 1)
  291. return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='',
  292. default_limits=(datemin, datemax))
  293. class PandasAutoDateFormatter(dates.AutoDateFormatter):
  294. def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'):
  295. dates.AutoDateFormatter.__init__(self, locator, tz, defaultfmt)
  296. # matplotlib.dates._UTC has no _utcoffset called by pandas
  297. if self._tz is dates.UTC:
  298. self._tz._utcoffset = self._tz.utcoffset(None)
  299. class PandasAutoDateLocator(dates.AutoDateLocator):
  300. def get_locator(self, dmin, dmax):
  301. 'Pick the best locator based on a distance.'
  302. _check_implicitly_registered()
  303. delta = relativedelta(dmax, dmin)
  304. num_days = (delta.years * 12.0 + delta.months) * 31.0 + delta.days
  305. num_sec = (delta.hours * 60.0 + delta.minutes) * 60.0 + delta.seconds
  306. tot_sec = num_days * 86400. + num_sec
  307. if abs(tot_sec) < self.minticks:
  308. self._freq = -1
  309. locator = MilliSecondLocator(self.tz)
  310. locator.set_axis(self.axis)
  311. locator.set_view_interval(*self.axis.get_view_interval())
  312. locator.set_data_interval(*self.axis.get_data_interval())
  313. return locator
  314. return dates.AutoDateLocator.get_locator(self, dmin, dmax)
  315. def _get_unit(self):
  316. return MilliSecondLocator.get_unit_generic(self._freq)
  317. class MilliSecondLocator(dates.DateLocator):
  318. UNIT = 1. / (24 * 3600 * 1000)
  319. def __init__(self, tz):
  320. dates.DateLocator.__init__(self, tz)
  321. self._interval = 1.
  322. def _get_unit(self):
  323. return self.get_unit_generic(-1)
  324. @staticmethod
  325. def get_unit_generic(freq):
  326. unit = dates.RRuleLocator.get_unit_generic(freq)
  327. if unit < 0:
  328. return MilliSecondLocator.UNIT
  329. return unit
  330. def __call__(self):
  331. # if no data have been set, this will tank with a ValueError
  332. _check_implicitly_registered()
  333. try:
  334. dmin, dmax = self.viewlim_to_dt()
  335. except ValueError:
  336. return []
  337. if dmin > dmax:
  338. dmax, dmin = dmin, dmax
  339. # We need to cap at the endpoints of valid datetime
  340. # TODO(wesm) unused?
  341. # delta = relativedelta(dmax, dmin)
  342. # try:
  343. # start = dmin - delta
  344. # except ValueError:
  345. # start = _from_ordinal(1.0)
  346. # try:
  347. # stop = dmax + delta
  348. # except ValueError:
  349. # # The magic number!
  350. # stop = _from_ordinal(3652059.9999999)
  351. nmax, nmin = dates.date2num((dmax, dmin))
  352. num = (nmax - nmin) * 86400 * 1000
  353. max_millis_ticks = 6
  354. for interval in [1, 10, 50, 100, 200, 500]:
  355. if num <= interval * (max_millis_ticks - 1):
  356. self._interval = interval
  357. break
  358. else:
  359. # We went through the whole loop without breaking, default to 1
  360. self._interval = 1000.
  361. estimate = (nmax - nmin) / (self._get_unit() * self._get_interval())
  362. if estimate > self.MAXTICKS * 2:
  363. raise RuntimeError(('MillisecondLocator estimated to generate '
  364. '{estimate:d} ticks from {dmin} to {dmax}: '
  365. 'exceeds Locator.MAXTICKS'
  366. '* 2 ({arg:d}) ').format(
  367. estimate=estimate, dmin=dmin, dmax=dmax,
  368. arg=self.MAXTICKS * 2))
  369. freq = '%dL' % self._get_interval()
  370. tz = self.tz.tzname(None)
  371. st = _from_ordinal(dates.date2num(dmin)) # strip tz
  372. ed = _from_ordinal(dates.date2num(dmax))
  373. all_dates = date_range(start=st, end=ed,
  374. freq=freq, tz=tz).astype(object)
  375. try:
  376. if len(all_dates) > 0:
  377. locs = self.raise_if_exceeds(dates.date2num(all_dates))
  378. return locs
  379. except Exception: # pragma: no cover
  380. pass
  381. lims = dates.date2num([dmin, dmax])
  382. return lims
  383. def _get_interval(self):
  384. return self._interval
  385. def autoscale(self):
  386. """
  387. Set the view limits to include the data range.
  388. """
  389. dmin, dmax = self.datalim_to_dt()
  390. if dmin > dmax:
  391. dmax, dmin = dmin, dmax
  392. # We need to cap at the endpoints of valid datetime
  393. # TODO(wesm): unused?
  394. # delta = relativedelta(dmax, dmin)
  395. # try:
  396. # start = dmin - delta
  397. # except ValueError:
  398. # start = _from_ordinal(1.0)
  399. # try:
  400. # stop = dmax + delta
  401. # except ValueError:
  402. # # The magic number!
  403. # stop = _from_ordinal(3652059.9999999)
  404. dmin, dmax = self.datalim_to_dt()
  405. vmin = dates.date2num(dmin)
  406. vmax = dates.date2num(dmax)
  407. return self.nonsingular(vmin, vmax)
  408. def _from_ordinal(x, tz=None):
  409. ix = int(x)
  410. dt = datetime.fromordinal(ix)
  411. remainder = float(x) - ix
  412. hour, remainder = divmod(24 * remainder, 1)
  413. minute, remainder = divmod(60 * remainder, 1)
  414. second, remainder = divmod(60 * remainder, 1)
  415. microsecond = int(1e6 * remainder)
  416. if microsecond < 10:
  417. microsecond = 0 # compensate for rounding errors
  418. dt = datetime(dt.year, dt.month, dt.day, int(hour), int(minute),
  419. int(second), microsecond)
  420. if tz is not None:
  421. dt = dt.astimezone(tz)
  422. if microsecond > 999990: # compensate for rounding errors
  423. dt += timedelta(microseconds=1e6 - microsecond)
  424. return dt
  425. # Fixed frequency dynamic tick locators and formatters
  426. # -------------------------------------------------------------------------
  427. # --- Locators ---
  428. # -------------------------------------------------------------------------
  429. def _get_default_annual_spacing(nyears):
  430. """
  431. Returns a default spacing between consecutive ticks for annual data.
  432. """
  433. if nyears < 11:
  434. (min_spacing, maj_spacing) = (1, 1)
  435. elif nyears < 20:
  436. (min_spacing, maj_spacing) = (1, 2)
  437. elif nyears < 50:
  438. (min_spacing, maj_spacing) = (1, 5)
  439. elif nyears < 100:
  440. (min_spacing, maj_spacing) = (5, 10)
  441. elif nyears < 200:
  442. (min_spacing, maj_spacing) = (5, 25)
  443. elif nyears < 600:
  444. (min_spacing, maj_spacing) = (10, 50)
  445. else:
  446. factor = nyears // 1000 + 1
  447. (min_spacing, maj_spacing) = (factor * 20, factor * 100)
  448. return (min_spacing, maj_spacing)
  449. def period_break(dates, period):
  450. """
  451. Returns the indices where the given period changes.
  452. Parameters
  453. ----------
  454. dates : PeriodIndex
  455. Array of intervals to monitor.
  456. period : string
  457. Name of the period to monitor.
  458. """
  459. current = getattr(dates, period)
  460. previous = getattr(dates - 1 * dates.freq, period)
  461. return np.nonzero(current - previous)[0]
  462. def has_level_label(label_flags, vmin):
  463. """
  464. Returns true if the ``label_flags`` indicate there is at least one label
  465. for this level.
  466. if the minimum view limit is not an exact integer, then the first tick
  467. label won't be shown, so we must adjust for that.
  468. """
  469. if label_flags.size == 0 or (label_flags.size == 1 and
  470. label_flags[0] == 0 and
  471. vmin % 1 > 0.0):
  472. return False
  473. else:
  474. return True
  475. def _daily_finder(vmin, vmax, freq):
  476. periodsperday = -1
  477. if freq >= FreqGroup.FR_HR:
  478. if freq == FreqGroup.FR_NS:
  479. periodsperday = 24 * 60 * 60 * 1000000000
  480. elif freq == FreqGroup.FR_US:
  481. periodsperday = 24 * 60 * 60 * 1000000
  482. elif freq == FreqGroup.FR_MS:
  483. periodsperday = 24 * 60 * 60 * 1000
  484. elif freq == FreqGroup.FR_SEC:
  485. periodsperday = 24 * 60 * 60
  486. elif freq == FreqGroup.FR_MIN:
  487. periodsperday = 24 * 60
  488. elif freq == FreqGroup.FR_HR:
  489. periodsperday = 24
  490. else: # pragma: no cover
  491. raise ValueError("unexpected frequency: {freq}".format(freq=freq))
  492. periodsperyear = 365 * periodsperday
  493. periodspermonth = 28 * periodsperday
  494. elif freq == FreqGroup.FR_BUS:
  495. periodsperyear = 261
  496. periodspermonth = 19
  497. elif freq == FreqGroup.FR_DAY:
  498. periodsperyear = 365
  499. periodspermonth = 28
  500. elif resolution.get_freq_group(freq) == FreqGroup.FR_WK:
  501. periodsperyear = 52
  502. periodspermonth = 3
  503. else: # pragma: no cover
  504. raise ValueError("unexpected frequency")
  505. # save this for later usage
  506. vmin_orig = vmin
  507. (vmin, vmax) = (Period(ordinal=int(vmin), freq=freq),
  508. Period(ordinal=int(vmax), freq=freq))
  509. span = vmax.ordinal - vmin.ordinal + 1
  510. dates_ = period_range(start=vmin, end=vmax, freq=freq)
  511. # Initialize the output
  512. info = np.zeros(span,
  513. dtype=[('val', np.int64), ('maj', bool),
  514. ('min', bool), ('fmt', '|S20')])
  515. info['val'][:] = dates_._ndarray_values
  516. info['fmt'][:] = ''
  517. info['maj'][[0, -1]] = True
  518. # .. and set some shortcuts
  519. info_maj = info['maj']
  520. info_min = info['min']
  521. info_fmt = info['fmt']
  522. def first_label(label_flags):
  523. if (label_flags[0] == 0) and (label_flags.size > 1) and \
  524. ((vmin_orig % 1) > 0.0):
  525. return label_flags[1]
  526. else:
  527. return label_flags[0]
  528. # Case 1. Less than a month
  529. if span <= periodspermonth:
  530. day_start = period_break(dates_, 'day')
  531. month_start = period_break(dates_, 'month')
  532. def _hour_finder(label_interval, force_year_start):
  533. _hour = dates_.hour
  534. _prev_hour = (dates_ - 1 * dates_.freq).hour
  535. hour_start = (_hour - _prev_hour) != 0
  536. info_maj[day_start] = True
  537. info_min[hour_start & (_hour % label_interval == 0)] = True
  538. year_start = period_break(dates_, 'year')
  539. info_fmt[hour_start & (_hour % label_interval == 0)] = '%H:%M'
  540. info_fmt[day_start] = '%H:%M\n%d-%b'
  541. info_fmt[year_start] = '%H:%M\n%d-%b\n%Y'
  542. if force_year_start and not has_level_label(year_start, vmin_orig):
  543. info_fmt[first_label(day_start)] = '%H:%M\n%d-%b\n%Y'
  544. def _minute_finder(label_interval):
  545. hour_start = period_break(dates_, 'hour')
  546. _minute = dates_.minute
  547. _prev_minute = (dates_ - 1 * dates_.freq).minute
  548. minute_start = (_minute - _prev_minute) != 0
  549. info_maj[hour_start] = True
  550. info_min[minute_start & (_minute % label_interval == 0)] = True
  551. year_start = period_break(dates_, 'year')
  552. info_fmt = info['fmt']
  553. info_fmt[minute_start & (_minute % label_interval == 0)] = '%H:%M'
  554. info_fmt[day_start] = '%H:%M\n%d-%b'
  555. info_fmt[year_start] = '%H:%M\n%d-%b\n%Y'
  556. def _second_finder(label_interval):
  557. minute_start = period_break(dates_, 'minute')
  558. _second = dates_.second
  559. _prev_second = (dates_ - 1 * dates_.freq).second
  560. second_start = (_second - _prev_second) != 0
  561. info['maj'][minute_start] = True
  562. info['min'][second_start & (_second % label_interval == 0)] = True
  563. year_start = period_break(dates_, 'year')
  564. info_fmt = info['fmt']
  565. info_fmt[second_start & (_second %
  566. label_interval == 0)] = '%H:%M:%S'
  567. info_fmt[day_start] = '%H:%M:%S\n%d-%b'
  568. info_fmt[year_start] = '%H:%M:%S\n%d-%b\n%Y'
  569. if span < periodsperday / 12000.0:
  570. _second_finder(1)
  571. elif span < periodsperday / 6000.0:
  572. _second_finder(2)
  573. elif span < periodsperday / 2400.0:
  574. _second_finder(5)
  575. elif span < periodsperday / 1200.0:
  576. _second_finder(10)
  577. elif span < periodsperday / 800.0:
  578. _second_finder(15)
  579. elif span < periodsperday / 400.0:
  580. _second_finder(30)
  581. elif span < periodsperday / 150.0:
  582. _minute_finder(1)
  583. elif span < periodsperday / 70.0:
  584. _minute_finder(2)
  585. elif span < periodsperday / 24.0:
  586. _minute_finder(5)
  587. elif span < periodsperday / 12.0:
  588. _minute_finder(15)
  589. elif span < periodsperday / 6.0:
  590. _minute_finder(30)
  591. elif span < periodsperday / 2.5:
  592. _hour_finder(1, False)
  593. elif span < periodsperday / 1.5:
  594. _hour_finder(2, False)
  595. elif span < periodsperday * 1.25:
  596. _hour_finder(3, False)
  597. elif span < periodsperday * 2.5:
  598. _hour_finder(6, True)
  599. elif span < periodsperday * 4:
  600. _hour_finder(12, True)
  601. else:
  602. info_maj[month_start] = True
  603. info_min[day_start] = True
  604. year_start = period_break(dates_, 'year')
  605. info_fmt = info['fmt']
  606. info_fmt[day_start] = '%d'
  607. info_fmt[month_start] = '%d\n%b'
  608. info_fmt[year_start] = '%d\n%b\n%Y'
  609. if not has_level_label(year_start, vmin_orig):
  610. if not has_level_label(month_start, vmin_orig):
  611. info_fmt[first_label(day_start)] = '%d\n%b\n%Y'
  612. else:
  613. info_fmt[first_label(month_start)] = '%d\n%b\n%Y'
  614. # Case 2. Less than three months
  615. elif span <= periodsperyear // 4:
  616. month_start = period_break(dates_, 'month')
  617. info_maj[month_start] = True
  618. if freq < FreqGroup.FR_HR:
  619. info['min'] = True
  620. else:
  621. day_start = period_break(dates_, 'day')
  622. info['min'][day_start] = True
  623. week_start = period_break(dates_, 'week')
  624. year_start = period_break(dates_, 'year')
  625. info_fmt[week_start] = '%d'
  626. info_fmt[month_start] = '\n\n%b'
  627. info_fmt[year_start] = '\n\n%b\n%Y'
  628. if not has_level_label(year_start, vmin_orig):
  629. if not has_level_label(month_start, vmin_orig):
  630. info_fmt[first_label(week_start)] = '\n\n%b\n%Y'
  631. else:
  632. info_fmt[first_label(month_start)] = '\n\n%b\n%Y'
  633. # Case 3. Less than 14 months ...............
  634. elif span <= 1.15 * periodsperyear:
  635. year_start = period_break(dates_, 'year')
  636. month_start = period_break(dates_, 'month')
  637. week_start = period_break(dates_, 'week')
  638. info_maj[month_start] = True
  639. info_min[week_start] = True
  640. info_min[year_start] = False
  641. info_min[month_start] = False
  642. info_fmt[month_start] = '%b'
  643. info_fmt[year_start] = '%b\n%Y'
  644. if not has_level_label(year_start, vmin_orig):
  645. info_fmt[first_label(month_start)] = '%b\n%Y'
  646. # Case 4. Less than 2.5 years ...............
  647. elif span <= 2.5 * periodsperyear:
  648. year_start = period_break(dates_, 'year')
  649. quarter_start = period_break(dates_, 'quarter')
  650. month_start = period_break(dates_, 'month')
  651. info_maj[quarter_start] = True
  652. info_min[month_start] = True
  653. info_fmt[quarter_start] = '%b'
  654. info_fmt[year_start] = '%b\n%Y'
  655. # Case 4. Less than 4 years .................
  656. elif span <= 4 * periodsperyear:
  657. year_start = period_break(dates_, 'year')
  658. month_start = period_break(dates_, 'month')
  659. info_maj[year_start] = True
  660. info_min[month_start] = True
  661. info_min[year_start] = False
  662. month_break = dates_[month_start].month
  663. jan_or_jul = month_start[(month_break == 1) | (month_break == 7)]
  664. info_fmt[jan_or_jul] = '%b'
  665. info_fmt[year_start] = '%b\n%Y'
  666. # Case 5. Less than 11 years ................
  667. elif span <= 11 * periodsperyear:
  668. year_start = period_break(dates_, 'year')
  669. quarter_start = period_break(dates_, 'quarter')
  670. info_maj[year_start] = True
  671. info_min[quarter_start] = True
  672. info_min[year_start] = False
  673. info_fmt[year_start] = '%Y'
  674. # Case 6. More than 12 years ................
  675. else:
  676. year_start = period_break(dates_, 'year')
  677. year_break = dates_[year_start].year
  678. nyears = span / periodsperyear
  679. (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears)
  680. major_idx = year_start[(year_break % maj_anndef == 0)]
  681. info_maj[major_idx] = True
  682. minor_idx = year_start[(year_break % min_anndef == 0)]
  683. info_min[minor_idx] = True
  684. info_fmt[major_idx] = '%Y'
  685. return info
  686. def _monthly_finder(vmin, vmax, freq):
  687. periodsperyear = 12
  688. vmin_orig = vmin
  689. (vmin, vmax) = (int(vmin), int(vmax))
  690. span = vmax - vmin + 1
  691. # Initialize the output
  692. info = np.zeros(span,
  693. dtype=[('val', int), ('maj', bool), ('min', bool),
  694. ('fmt', '|S8')])
  695. info['val'] = np.arange(vmin, vmax + 1)
  696. dates_ = info['val']
  697. info['fmt'] = ''
  698. year_start = (dates_ % 12 == 0).nonzero()[0]
  699. info_maj = info['maj']
  700. info_fmt = info['fmt']
  701. if span <= 1.15 * periodsperyear:
  702. info_maj[year_start] = True
  703. info['min'] = True
  704. info_fmt[:] = '%b'
  705. info_fmt[year_start] = '%b\n%Y'
  706. if not has_level_label(year_start, vmin_orig):
  707. if dates_.size > 1:
  708. idx = 1
  709. else:
  710. idx = 0
  711. info_fmt[idx] = '%b\n%Y'
  712. elif span <= 2.5 * periodsperyear:
  713. quarter_start = (dates_ % 3 == 0).nonzero()
  714. info_maj[year_start] = True
  715. # TODO: Check the following : is it really info['fmt'] ?
  716. info['fmt'][quarter_start] = True
  717. info['min'] = True
  718. info_fmt[quarter_start] = '%b'
  719. info_fmt[year_start] = '%b\n%Y'
  720. elif span <= 4 * periodsperyear:
  721. info_maj[year_start] = True
  722. info['min'] = True
  723. jan_or_jul = (dates_ % 12 == 0) | (dates_ % 12 == 6)
  724. info_fmt[jan_or_jul] = '%b'
  725. info_fmt[year_start] = '%b\n%Y'
  726. elif span <= 11 * periodsperyear:
  727. quarter_start = (dates_ % 3 == 0).nonzero()
  728. info_maj[year_start] = True
  729. info['min'][quarter_start] = True
  730. info_fmt[year_start] = '%Y'
  731. else:
  732. nyears = span / periodsperyear
  733. (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears)
  734. years = dates_[year_start] // 12 + 1
  735. major_idx = year_start[(years % maj_anndef == 0)]
  736. info_maj[major_idx] = True
  737. info['min'][year_start[(years % min_anndef == 0)]] = True
  738. info_fmt[major_idx] = '%Y'
  739. return info
  740. def _quarterly_finder(vmin, vmax, freq):
  741. periodsperyear = 4
  742. vmin_orig = vmin
  743. (vmin, vmax) = (int(vmin), int(vmax))
  744. span = vmax - vmin + 1
  745. info = np.zeros(span,
  746. dtype=[('val', int), ('maj', bool), ('min', bool),
  747. ('fmt', '|S8')])
  748. info['val'] = np.arange(vmin, vmax + 1)
  749. info['fmt'] = ''
  750. dates_ = info['val']
  751. info_maj = info['maj']
  752. info_fmt = info['fmt']
  753. year_start = (dates_ % 4 == 0).nonzero()[0]
  754. if span <= 3.5 * periodsperyear:
  755. info_maj[year_start] = True
  756. info['min'] = True
  757. info_fmt[:] = 'Q%q'
  758. info_fmt[year_start] = 'Q%q\n%F'
  759. if not has_level_label(year_start, vmin_orig):
  760. if dates_.size > 1:
  761. idx = 1
  762. else:
  763. idx = 0
  764. info_fmt[idx] = 'Q%q\n%F'
  765. elif span <= 11 * periodsperyear:
  766. info_maj[year_start] = True
  767. info['min'] = True
  768. info_fmt[year_start] = '%F'
  769. else:
  770. years = dates_[year_start] // 4 + 1
  771. nyears = span / periodsperyear
  772. (min_anndef, maj_anndef) = _get_default_annual_spacing(nyears)
  773. major_idx = year_start[(years % maj_anndef == 0)]
  774. info_maj[major_idx] = True
  775. info['min'][year_start[(years % min_anndef == 0)]] = True
  776. info_fmt[major_idx] = '%F'
  777. return info
  778. def _annual_finder(vmin, vmax, freq):
  779. (vmin, vmax) = (int(vmin), int(vmax + 1))
  780. span = vmax - vmin + 1
  781. info = np.zeros(span,
  782. dtype=[('val', int), ('maj', bool), ('min', bool),
  783. ('fmt', '|S8')])
  784. info['val'] = np.arange(vmin, vmax + 1)
  785. info['fmt'] = ''
  786. dates_ = info['val']
  787. (min_anndef, maj_anndef) = _get_default_annual_spacing(span)
  788. major_idx = dates_ % maj_anndef == 0
  789. info['maj'][major_idx] = True
  790. info['min'][(dates_ % min_anndef == 0)] = True
  791. info['fmt'][major_idx] = '%Y'
  792. return info
  793. def get_finder(freq):
  794. if isinstance(freq, compat.string_types):
  795. freq = get_freq(freq)
  796. fgroup = resolution.get_freq_group(freq)
  797. if fgroup == FreqGroup.FR_ANN:
  798. return _annual_finder
  799. elif fgroup == FreqGroup.FR_QTR:
  800. return _quarterly_finder
  801. elif freq == FreqGroup.FR_MTH:
  802. return _monthly_finder
  803. elif ((freq >= FreqGroup.FR_BUS) or fgroup == FreqGroup.FR_WK):
  804. return _daily_finder
  805. else: # pragma: no cover
  806. errmsg = "Unsupported frequency: {freq}".format(freq=freq)
  807. raise NotImplementedError(errmsg)
  808. class TimeSeries_DateLocator(Locator):
  809. """
  810. Locates the ticks along an axis controlled by a :class:`Series`.
  811. Parameters
  812. ----------
  813. freq : {var}
  814. Valid frequency specifier.
  815. minor_locator : {False, True}, optional
  816. Whether the locator is for minor ticks (True) or not.
  817. dynamic_mode : {True, False}, optional
  818. Whether the locator should work in dynamic mode.
  819. base : {int}, optional
  820. quarter : {int}, optional
  821. month : {int}, optional
  822. day : {int}, optional
  823. """
  824. def __init__(self, freq, minor_locator=False, dynamic_mode=True,
  825. base=1, quarter=1, month=1, day=1, plot_obj=None):
  826. if isinstance(freq, compat.string_types):
  827. freq = get_freq(freq)
  828. self.freq = freq
  829. self.base = base
  830. (self.quarter, self.month, self.day) = (quarter, month, day)
  831. self.isminor = minor_locator
  832. self.isdynamic = dynamic_mode
  833. self.offset = 0
  834. self.plot_obj = plot_obj
  835. self.finder = get_finder(freq)
  836. def _get_default_locs(self, vmin, vmax):
  837. "Returns the default locations of ticks."
  838. if self.plot_obj.date_axis_info is None:
  839. self.plot_obj.date_axis_info = self.finder(vmin, vmax, self.freq)
  840. locator = self.plot_obj.date_axis_info
  841. if self.isminor:
  842. return np.compress(locator['min'], locator['val'])
  843. return np.compress(locator['maj'], locator['val'])
  844. def __call__(self):
  845. 'Return the locations of the ticks.'
  846. # axis calls Locator.set_axis inside set_m<xxxx>_formatter
  847. _check_implicitly_registered()
  848. vi = tuple(self.axis.get_view_interval())
  849. if vi != self.plot_obj.view_interval:
  850. self.plot_obj.date_axis_info = None
  851. self.plot_obj.view_interval = vi
  852. vmin, vmax = vi
  853. if vmax < vmin:
  854. vmin, vmax = vmax, vmin
  855. if self.isdynamic:
  856. locs = self._get_default_locs(vmin, vmax)
  857. else: # pragma: no cover
  858. base = self.base
  859. (d, m) = divmod(vmin, base)
  860. vmin = (d + 1) * base
  861. locs = lrange(vmin, vmax + 1, base)
  862. return locs
  863. def autoscale(self):
  864. """
  865. Sets the view limits to the nearest multiples of base that contain the
  866. data.
  867. """
  868. # requires matplotlib >= 0.98.0
  869. (vmin, vmax) = self.axis.get_data_interval()
  870. locs = self._get_default_locs(vmin, vmax)
  871. (vmin, vmax) = locs[[0, -1]]
  872. if vmin == vmax:
  873. vmin -= 1
  874. vmax += 1
  875. return nonsingular(vmin, vmax)
  876. # -------------------------------------------------------------------------
  877. # --- Formatter ---
  878. # -------------------------------------------------------------------------
  879. class TimeSeries_DateFormatter(Formatter):
  880. """
  881. Formats the ticks along an axis controlled by a :class:`PeriodIndex`.
  882. Parameters
  883. ----------
  884. freq : {int, string}
  885. Valid frequency specifier.
  886. minor_locator : {False, True}
  887. Whether the current formatter should apply to minor ticks (True) or
  888. major ticks (False).
  889. dynamic_mode : {True, False}
  890. Whether the formatter works in dynamic mode or not.
  891. """
  892. def __init__(self, freq, minor_locator=False, dynamic_mode=True,
  893. plot_obj=None):
  894. if isinstance(freq, compat.string_types):
  895. freq = get_freq(freq)
  896. self.format = None
  897. self.freq = freq
  898. self.locs = []
  899. self.formatdict = None
  900. self.isminor = minor_locator
  901. self.isdynamic = dynamic_mode
  902. self.offset = 0
  903. self.plot_obj = plot_obj
  904. self.finder = get_finder(freq)
  905. def _set_default_format(self, vmin, vmax):
  906. "Returns the default ticks spacing."
  907. if self.plot_obj.date_axis_info is None:
  908. self.plot_obj.date_axis_info = self.finder(vmin, vmax, self.freq)
  909. info = self.plot_obj.date_axis_info
  910. if self.isminor:
  911. format = np.compress(info['min'] & np.logical_not(info['maj']),
  912. info)
  913. else:
  914. format = np.compress(info['maj'], info)
  915. self.formatdict = {x: f for (x, _, _, f) in format}
  916. return self.formatdict
  917. def set_locs(self, locs):
  918. 'Sets the locations of the ticks'
  919. # don't actually use the locs. This is just needed to work with
  920. # matplotlib. Force to use vmin, vmax
  921. _check_implicitly_registered()
  922. self.locs = locs
  923. (vmin, vmax) = vi = tuple(self.axis.get_view_interval())
  924. if vi != self.plot_obj.view_interval:
  925. self.plot_obj.date_axis_info = None
  926. self.plot_obj.view_interval = vi
  927. if vmax < vmin:
  928. (vmin, vmax) = (vmax, vmin)
  929. self._set_default_format(vmin, vmax)
  930. def __call__(self, x, pos=0):
  931. _check_implicitly_registered()
  932. if self.formatdict is None:
  933. return ''
  934. else:
  935. fmt = self.formatdict.pop(x, '')
  936. return Period(ordinal=int(x), freq=self.freq).strftime(fmt)
  937. class TimeSeries_TimedeltaFormatter(Formatter):
  938. """
  939. Formats the ticks along an axis controlled by a :class:`TimedeltaIndex`.
  940. """
  941. @staticmethod
  942. def format_timedelta_ticks(x, pos, n_decimals):
  943. """
  944. Convert seconds to 'D days HH:MM:SS.F'
  945. """
  946. s, ns = divmod(x, 1e9)
  947. m, s = divmod(s, 60)
  948. h, m = divmod(m, 60)
  949. d, h = divmod(h, 24)
  950. decimals = int(ns * 10**(n_decimals - 9))
  951. s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s))
  952. if n_decimals > 0:
  953. s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals)
  954. if d != 0:
  955. s = '{:d} days '.format(int(d)) + s
  956. return s
  957. def __call__(self, x, pos=0):
  958. _check_implicitly_registered()
  959. (vmin, vmax) = tuple(self.axis.get_view_interval())
  960. n_decimals = int(np.ceil(np.log10(100 * 1e9 / (vmax - vmin))))
  961. if n_decimals > 9:
  962. n_decimals = 9
  963. return self.format_timedelta_ticks(x, pos, n_decimals)