converter.py 36 KB

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