1""" 2Matplotlib provides sophisticated date plotting capabilities, standing on the 3shoulders of python :mod:`datetime` and the add-on module :mod:`dateutil`. 4 5.. _date-format: 6 7Matplotlib date format 8---------------------- 9 10Matplotlib represents dates using floating point numbers specifying the number 11of days since a default epoch of 1970-01-01 UTC; for example, 121970-01-01, 06:00 is the floating point number 0.25. The formatters and 13locators require the use of `datetime.datetime` objects, so only dates between 14year 0001 and 9999 can be represented. Microsecond precision 15is achievable for (approximately) 70 years on either side of the epoch, and 1620 microseconds for the rest of the allowable range of dates (year 0001 to 179999). The epoch can be changed at import time via `.dates.set_epoch` or 18:rc:`dates.epoch` to other dates if necessary; see 19:doc:`/gallery/ticks_and_spines/date_precision_and_epochs` for a discussion. 20 21.. note:: 22 23 Before Matplotlib 3.3, the epoch was 0000-12-31 which lost modern 24 microsecond precision and also made the default axis limit of 0 an invalid 25 datetime. In 3.3 the epoch was changed as above. To convert old 26 ordinal floats to the new epoch, users can do:: 27 28 new_ordinal = old_ordinal + mdates.date2num(np.datetime64('0000-12-31')) 29 30 31There are a number of helper functions to convert between :mod:`datetime` 32objects and Matplotlib dates: 33 34.. currentmodule:: matplotlib.dates 35 36.. autosummary:: 37 :nosignatures: 38 39 datestr2num 40 date2num 41 num2date 42 num2timedelta 43 drange 44 set_epoch 45 get_epoch 46 47.. note:: 48 49 Like Python's `datetime.datetime`, Matplotlib uses the Gregorian calendar 50 for all conversions between dates and floating point numbers. This practice 51 is not universal, and calendar differences can cause confusing 52 differences between what Python and Matplotlib give as the number of days 53 since 0001-01-01 and what other software and databases yield. For 54 example, the US Naval Observatory uses a calendar that switches 55 from Julian to Gregorian in October, 1582. Hence, using their 56 calculator, the number of days between 0001-01-01 and 2006-04-01 is 57 732403, whereas using the Gregorian calendar via the datetime 58 module we find:: 59 60 In [1]: date(2006, 4, 1).toordinal() - date(1, 1, 1).toordinal() 61 Out[1]: 732401 62 63All the Matplotlib date converters, tickers and formatters are timezone aware. 64If no explicit timezone is provided, :rc:`timezone` is assumed. If you want to 65use a custom time zone, pass a `datetime.tzinfo` instance with the tz keyword 66argument to `num2date`, `~.Axes.plot_date`, and any custom date tickers or 67locators you create. 68 69A wide range of specific and general purpose date tick locators and 70formatters are provided in this module. See 71:mod:`matplotlib.ticker` for general information on tick locators 72and formatters. These are described below. 73 74The dateutil_ module provides additional code to handle date ticking, making it 75easy to place ticks on any kinds of dates. See examples below. 76 77.. _dateutil: https://dateutil.readthedocs.io 78 79Date tickers 80------------ 81 82Most of the date tickers can locate single or multiple values. For example:: 83 84 # import constants for the days of the week 85 from matplotlib.dates import MO, TU, WE, TH, FR, SA, SU 86 87 # tick on mondays every week 88 loc = WeekdayLocator(byweekday=MO, tz=tz) 89 90 # tick on mondays and saturdays 91 loc = WeekdayLocator(byweekday=(MO, SA)) 92 93In addition, most of the constructors take an interval argument:: 94 95 # tick on mondays every second week 96 loc = WeekdayLocator(byweekday=MO, interval=2) 97 98The rrule locator allows completely general date ticking:: 99 100 # tick every 5th easter 101 rule = rrulewrapper(YEARLY, byeaster=1, interval=5) 102 loc = RRuleLocator(rule) 103 104The available date tickers are: 105 106* `MicrosecondLocator`: Locate microseconds. 107 108* `SecondLocator`: Locate seconds. 109 110* `MinuteLocator`: Locate minutes. 111 112* `HourLocator`: Locate hours. 113 114* `DayLocator`: Locate specified days of the month. 115 116* `WeekdayLocator`: Locate days of the week, e.g., MO, TU. 117 118* `MonthLocator`: Locate months, e.g., 7 for July. 119 120* `YearLocator`: Locate years that are multiples of base. 121 122* `RRuleLocator`: Locate using a `matplotlib.dates.rrulewrapper`. 123 `.rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` which 124 allow almost arbitrary date tick specifications. See :doc:`rrule example 125 </gallery/ticks_and_spines/date_demo_rrule>`. 126 127* `AutoDateLocator`: On autoscale, this class picks the best `DateLocator` 128 (e.g., `RRuleLocator`) to set the view limits and the tick locations. If 129 called with ``interval_multiples=True`` it will make ticks line up with 130 sensible multiples of the tick intervals. E.g. if the interval is 4 hours, 131 it will pick hours 0, 4, 8, etc as ticks. This behaviour is not guaranteed 132 by default. 133 134Date formatters 135--------------- 136 137The available date formatters are: 138 139* `AutoDateFormatter`: attempts to figure out the best format to use. This is 140 most useful when used with the `AutoDateLocator`. 141 142* `ConciseDateFormatter`: also attempts to figure out the best format to use, 143 and to make the format as compact as possible while still having complete 144 date information. This is most useful when used with the `AutoDateLocator`. 145 146* `DateFormatter`: use `~datetime.datetime.strftime` format strings. 147 148* `IndexDateFormatter`: date plots with implicit *x* indexing. 149""" 150 151import datetime 152import functools 153import logging 154import math 155import re 156 157from dateutil.rrule import (rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY, 158 MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, 159 SECONDLY) 160from dateutil.relativedelta import relativedelta 161import dateutil.parser 162import dateutil.tz 163import numpy as np 164 165import matplotlib as mpl 166from matplotlib import _api, cbook, ticker, units 167 168__all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange', 169 'epoch2num', 'num2epoch', 'set_epoch', 'get_epoch', 'DateFormatter', 170 'ConciseDateFormatter', 'IndexDateFormatter', 'AutoDateFormatter', 171 'DateLocator', 'RRuleLocator', 'AutoDateLocator', 'YearLocator', 172 'MonthLocator', 'WeekdayLocator', 173 'DayLocator', 'HourLocator', 'MinuteLocator', 174 'SecondLocator', 'MicrosecondLocator', 175 'rrule', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU', 176 'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 177 'HOURLY', 'MINUTELY', 'SECONDLY', 'MICROSECONDLY', 'relativedelta', 178 'DateConverter', 'ConciseDateConverter') 179 180 181_log = logging.getLogger(__name__) 182UTC = datetime.timezone.utc 183 184 185def _get_rc_timezone(): 186 """Retrieve the preferred timezone from the rcParams dictionary.""" 187 s = mpl.rcParams['timezone'] 188 if s == 'UTC': 189 return UTC 190 return dateutil.tz.gettz(s) 191 192 193""" 194Time-related constants. 195""" 196EPOCH_OFFSET = float(datetime.datetime(1970, 1, 1).toordinal()) 197# EPOCH_OFFSET is not used by matplotlib 198JULIAN_OFFSET = 1721424.5 # Julian date at 0000-12-31 199# note that the Julian day epoch is achievable w/ 200# np.datetime64('-4713-11-24T12:00:00'); datetime64 is proleptic 201# Gregorian and BC has a one-year offset. So 202# np.datetime64('0000-12-31') - np.datetime64('-4713-11-24T12:00') = 1721424.5 203# Ref: https://en.wikipedia.org/wiki/Julian_day 204MICROSECONDLY = SECONDLY + 1 205HOURS_PER_DAY = 24. 206MIN_PER_HOUR = 60. 207SEC_PER_MIN = 60. 208MONTHS_PER_YEAR = 12. 209 210DAYS_PER_WEEK = 7. 211DAYS_PER_MONTH = 30. 212DAYS_PER_YEAR = 365.0 213 214MINUTES_PER_DAY = MIN_PER_HOUR * HOURS_PER_DAY 215 216SEC_PER_HOUR = SEC_PER_MIN * MIN_PER_HOUR 217SEC_PER_DAY = SEC_PER_HOUR * HOURS_PER_DAY 218SEC_PER_WEEK = SEC_PER_DAY * DAYS_PER_WEEK 219 220MUSECONDS_PER_DAY = 1e6 * SEC_PER_DAY 221 222MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = ( 223 MO, TU, WE, TH, FR, SA, SU) 224WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) 225 226# default epoch: passed to np.datetime64... 227_epoch = None 228 229 230def _reset_epoch_test_example(): 231 """ 232 Reset the Matplotlib date epoch so it can be set again. 233 234 Only for use in tests and examples. 235 """ 236 global _epoch 237 _epoch = None 238 239 240def set_epoch(epoch): 241 """ 242 Set the epoch (origin for dates) for datetime calculations. 243 244 The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00). 245 246 If microsecond accuracy is desired, the date being plotted needs to be 247 within approximately 70 years of the epoch. Matplotlib internally 248 represents dates as days since the epoch, so floating point dynamic 249 range needs to be within a factor of 2^52. 250 251 `~.dates.set_epoch` must be called before any dates are converted 252 (i.e. near the import section) or a RuntimeError will be raised. 253 254 See also :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. 255 256 Parameters 257 ---------- 258 epoch : str 259 valid UTC date parsable by `numpy.datetime64` (do not include 260 timezone). 261 262 """ 263 global _epoch 264 if _epoch is not None: 265 raise RuntimeError('set_epoch must be called before dates plotted.') 266 _epoch = epoch 267 268 269def get_epoch(): 270 """ 271 Get the epoch used by `.dates`. 272 273 Returns 274 ------- 275 epoch : str 276 String for the epoch (parsable by `numpy.datetime64`). 277 """ 278 global _epoch 279 280 if _epoch is None: 281 _epoch = mpl.rcParams['date.epoch'] 282 return _epoch 283 284 285def _dt64_to_ordinalf(d): 286 """ 287 Convert `numpy.datetime64` or an ndarray of those types to Gregorian 288 date as UTC float relative to the epoch (see `.get_epoch`). Roundoff 289 is float64 precision. Practically: microseconds for dates between 290 290301 BC, 294241 AD, milliseconds for larger dates 291 (see `numpy.datetime64`). 292 """ 293 294 # the "extra" ensures that we at least allow the dynamic range out to 295 # seconds. That should get out to +/-2e11 years. 296 dseconds = d.astype('datetime64[s]') 297 extra = (d - dseconds).astype('timedelta64[ns]') 298 t0 = np.datetime64(get_epoch(), 's') 299 dt = (dseconds - t0).astype(np.float64) 300 dt += extra.astype(np.float64) / 1.0e9 301 dt = dt / SEC_PER_DAY 302 303 NaT_int = np.datetime64('NaT').astype(np.int64) 304 d_int = d.astype(np.int64) 305 try: 306 dt[d_int == NaT_int] = np.nan 307 except TypeError: 308 if d_int == NaT_int: 309 dt = np.nan 310 return dt 311 312 313def _from_ordinalf(x, tz=None): 314 """ 315 Convert Gregorian float of the date, preserving hours, minutes, 316 seconds and microseconds. Return value is a `.datetime`. 317 318 The input date *x* is a float in ordinal days at UTC, and the output will 319 be the specified `.datetime` object corresponding to that time in 320 timezone *tz*, or if *tz* is ``None``, in the timezone specified in 321 :rc:`timezone`. 322 """ 323 324 if tz is None: 325 tz = _get_rc_timezone() 326 327 dt = (np.datetime64(get_epoch()) + 328 np.timedelta64(int(np.round(x * MUSECONDS_PER_DAY)), 'us')) 329 if dt < np.datetime64('0001-01-01') or dt >= np.datetime64('10000-01-01'): 330 raise ValueError(f'Date ordinal {x} converts to {dt} (using ' 331 f'epoch {get_epoch()}), but Matplotlib dates must be ' 332 'between year 0001 and 9999.') 333 # convert from datetime64 to datetime: 334 dt = dt.tolist() 335 336 # datetime64 is always UTC: 337 dt = dt.replace(tzinfo=dateutil.tz.gettz('UTC')) 338 # but maybe we are working in a different timezone so move. 339 dt = dt.astimezone(tz) 340 # fix round off errors 341 if np.abs(x) > 70 * 365: 342 # if x is big, round off to nearest twenty microseconds. 343 # This avoids floating point roundoff error 344 ms = round(dt.microsecond / 20) * 20 345 if ms == 1000000: 346 dt = dt.replace(microsecond=0) + datetime.timedelta(seconds=1) 347 else: 348 dt = dt.replace(microsecond=ms) 349 350 return dt 351 352 353# a version of _from_ordinalf that can operate on numpy arrays 354_from_ordinalf_np_vectorized = np.vectorize(_from_ordinalf, otypes="O") 355 356 357# a version of dateutil.parser.parse that can operate on numpy arrays 358_dateutil_parser_parse_np_vectorized = np.vectorize(dateutil.parser.parse) 359 360 361def datestr2num(d, default=None): 362 """ 363 Convert a date string to a datenum using `dateutil.parser.parse`. 364 365 Parameters 366 ---------- 367 d : str or sequence of str 368 The dates to convert. 369 370 default : datetime.datetime, optional 371 The default date to use when fields are missing in *d*. 372 """ 373 if isinstance(d, str): 374 dt = dateutil.parser.parse(d, default=default) 375 return date2num(dt) 376 else: 377 if default is not None: 378 d = [dateutil.parser.parse(s, default=default) for s in d] 379 d = np.asarray(d) 380 if not d.size: 381 return d 382 return date2num(_dateutil_parser_parse_np_vectorized(d)) 383 384 385def date2num(d): 386 """ 387 Convert datetime objects to Matplotlib dates. 388 389 Parameters 390 ---------- 391 d : `datetime.datetime` or `numpy.datetime64` or sequences of these 392 393 Returns 394 ------- 395 float or sequence of floats 396 Number of days since the epoch. See `.get_epoch` for the 397 epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`. If 398 the epoch is "1970-01-01T00:00:00" (default) then noon Jan 1 1970 399 ("1970-01-01T12:00:00") returns 0.5. 400 401 Notes 402 ----- 403 The Gregorian calendar is assumed; this is not universal practice. 404 For details see the module docstring. 405 """ 406 if hasattr(d, "values"): 407 # this unpacks pandas series or dataframes... 408 d = d.values 409 410 # make an iterable, but save state to unpack later: 411 iterable = np.iterable(d) 412 if not iterable: 413 d = [d] 414 415 d = np.asarray(d) 416 # convert to datetime64 arrays, if not already: 417 if not np.issubdtype(d.dtype, np.datetime64): 418 # datetime arrays 419 if not d.size: 420 # deals with an empty array... 421 return d 422 tzi = getattr(d[0], 'tzinfo', None) 423 if tzi is not None: 424 # make datetime naive: 425 d = [dt.astimezone(UTC).replace(tzinfo=None) for dt in d] 426 d = np.asarray(d) 427 d = d.astype('datetime64[us]') 428 429 d = _dt64_to_ordinalf(d) 430 431 return d if iterable else d[0] 432 433 434def julian2num(j): 435 """ 436 Convert a Julian date (or sequence) to a Matplotlib date (or sequence). 437 438 Parameters 439 ---------- 440 j : float or sequence of floats 441 Julian dates (days relative to 4713 BC Jan 1, 12:00:00 Julian 442 calendar or 4714 BC Nov 24, 12:00:00, proleptic Gregorian calendar). 443 444 Returns 445 ------- 446 float or sequence of floats 447 Matplotlib dates (days relative to `.get_epoch`). 448 """ 449 ep = np.datetime64(get_epoch(), 'h').astype(float) / 24. 450 ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24. 451 # Julian offset defined above is relative to 0000-12-31, but we need 452 # relative to our current epoch: 453 dt = JULIAN_OFFSET - ep0 + ep 454 return np.subtract(j, dt) # Handles both scalar & nonscalar j. 455 456 457def num2julian(n): 458 """ 459 Convert a Matplotlib date (or sequence) to a Julian date (or sequence). 460 461 Parameters 462 ---------- 463 n : float or sequence of floats 464 Matplotlib dates (days relative to `.get_epoch`). 465 466 Returns 467 ------- 468 float or sequence of floats 469 Julian dates (days relative to 4713 BC Jan 1, 12:00:00). 470 """ 471 ep = np.datetime64(get_epoch(), 'h').astype(float) / 24. 472 ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24. 473 # Julian offset defined above is relative to 0000-12-31, but we need 474 # relative to our current epoch: 475 dt = JULIAN_OFFSET - ep0 + ep 476 return np.add(n, dt) # Handles both scalar & nonscalar j. 477 478 479def num2date(x, tz=None): 480 """ 481 Convert Matplotlib dates to `~datetime.datetime` objects. 482 483 Parameters 484 ---------- 485 x : float or sequence of floats 486 Number of days (fraction part represents hours, minutes, seconds) 487 since the epoch. See `.get_epoch` for the 488 epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`. 489 tz : str, default: :rc:`timezone` 490 Timezone of *x*. 491 492 Returns 493 ------- 494 `~datetime.datetime` or sequence of `~datetime.datetime` 495 Dates are returned in timezone *tz*. 496 497 If *x* is a sequence, a sequence of `~datetime.datetime` objects will 498 be returned. 499 500 Notes 501 ----- 502 The addition of one here is a historical artifact. Also, note that the 503 Gregorian calendar is assumed; this is not universal practice. 504 For details, see the module docstring. 505 """ 506 if tz is None: 507 tz = _get_rc_timezone() 508 return _from_ordinalf_np_vectorized(x, tz).tolist() 509 510 511_ordinalf_to_timedelta_np_vectorized = np.vectorize( 512 lambda x: datetime.timedelta(days=x), otypes="O") 513 514 515def num2timedelta(x): 516 """ 517 Convert number of days to a `~datetime.timedelta` object. 518 519 If *x* is a sequence, a sequence of `~datetime.timedelta` objects will 520 be returned. 521 522 Parameters 523 ---------- 524 x : float, sequence of floats 525 Number of days. The fraction part represents hours, minutes, seconds. 526 527 Returns 528 ------- 529 `datetime.timedelta` or list[`datetime.timedelta`] 530 """ 531 return _ordinalf_to_timedelta_np_vectorized(x).tolist() 532 533 534def drange(dstart, dend, delta): 535 """ 536 Return a sequence of equally spaced Matplotlib dates. 537 538 The dates start at *dstart* and reach up to, but not including *dend*. 539 They are spaced by *delta*. 540 541 Parameters 542 ---------- 543 dstart, dend : `~datetime.datetime` 544 The date limits. 545 delta : `datetime.timedelta` 546 Spacing of the dates. 547 548 Returns 549 ------- 550 `numpy.array` 551 A list floats representing Matplotlib dates. 552 553 """ 554 f1 = date2num(dstart) 555 f2 = date2num(dend) 556 step = delta.total_seconds() / SEC_PER_DAY 557 558 # calculate the difference between dend and dstart in times of delta 559 num = int(np.ceil((f2 - f1) / step)) 560 561 # calculate end of the interval which will be generated 562 dinterval_end = dstart + num * delta 563 564 # ensure, that an half open interval will be generated [dstart, dend) 565 if dinterval_end >= dend: 566 # if the endpoint is greater than dend, just subtract one delta 567 dinterval_end -= delta 568 num -= 1 569 570 f2 = date2num(dinterval_end) # new float-endpoint 571 return np.linspace(f1, f2, num + 1) 572 573 574def _wrap_in_tex(text): 575 p = r'([a-zA-Z]+)' 576 ret_text = re.sub(p, r'}$\1$\\mathdefault{', text) 577 578 # Braces ensure dashes are not spaced like binary operators. 579 ret_text = '$\\mathdefault{'+ret_text.replace('-', '{-}')+'}$' 580 ret_text = ret_text.replace('$\\mathdefault{}$', '') 581 return ret_text 582 583 584## date tickers and formatters ### 585 586 587class DateFormatter(ticker.Formatter): 588 """ 589 Format a tick (in days since the epoch) with a 590 `~datetime.datetime.strftime` format string. 591 """ 592 593 @_api.deprecated("3.3") 594 @property 595 def illegal_s(self): 596 return re.compile(r"((^|[^%])(%%)*%s)") 597 598 def __init__(self, fmt, tz=None, *, usetex=None): 599 """ 600 Parameters 601 ---------- 602 fmt : str 603 `~datetime.datetime.strftime` format string 604 tz : `datetime.tzinfo`, default: :rc:`timezone` 605 Ticks timezone. 606 usetex : bool, default: :rc:`text.usetex` 607 To enable/disable the use of TeX's math mode for rendering the 608 results of the formatter. 609 """ 610 if tz is None: 611 tz = _get_rc_timezone() 612 self.fmt = fmt 613 self.tz = tz 614 self._usetex = (usetex if usetex is not None else 615 mpl.rcParams['text.usetex']) 616 617 def __call__(self, x, pos=0): 618 result = num2date(x, self.tz).strftime(self.fmt) 619 return _wrap_in_tex(result) if self._usetex else result 620 621 def set_tzinfo(self, tz): 622 self.tz = tz 623 624 625@_api.deprecated("3.3") 626class IndexDateFormatter(ticker.Formatter): 627 """Use with `.IndexLocator` to cycle format strings by index.""" 628 629 def __init__(self, t, fmt, tz=None): 630 """ 631 Parameters 632 ---------- 633 t : list of float 634 A sequence of dates (floating point days). 635 fmt : str 636 A `~datetime.datetime.strftime` format string. 637 """ 638 if tz is None: 639 tz = _get_rc_timezone() 640 self.t = t 641 self.fmt = fmt 642 self.tz = tz 643 644 def __call__(self, x, pos=0): 645 """Return the label for time *x* at position *pos*.""" 646 ind = int(round(x)) 647 if ind >= len(self.t) or ind <= 0: 648 return '' 649 return num2date(self.t[ind], self.tz).strftime(self.fmt) 650 651 652class ConciseDateFormatter(ticker.Formatter): 653 """ 654 A `.Formatter` which attempts to figure out the best format to use for the 655 date, and to make it as compact as possible, but still be complete. This is 656 most useful when used with the `AutoDateLocator`:: 657 658 >>> locator = AutoDateLocator() 659 >>> formatter = ConciseDateFormatter(locator) 660 661 Parameters 662 ---------- 663 locator : `.ticker.Locator` 664 Locator that this axis is using. 665 666 tz : str, optional 667 Passed to `.dates.date2num`. 668 669 formats : list of 6 strings, optional 670 Format strings for 6 levels of tick labelling: mostly years, 671 months, days, hours, minutes, and seconds. Strings use 672 the same format codes as `~datetime.datetime.strftime`. Default is 673 ``['%Y', '%b', '%d', '%H:%M', '%H:%M', '%S.%f']`` 674 675 zero_formats : list of 6 strings, optional 676 Format strings for tick labels that are "zeros" for a given tick 677 level. For instance, if most ticks are months, ticks around 1 Jan 2005 678 will be labeled "Dec", "2005", "Feb". The default is 679 ``['', '%Y', '%b', '%b-%d', '%H:%M', '%H:%M']`` 680 681 offset_formats : list of 6 strings, optional 682 Format strings for the 6 levels that is applied to the "offset" 683 string found on the right side of an x-axis, or top of a y-axis. 684 Combined with the tick labels this should completely specify the 685 date. The default is:: 686 687 ['', '%Y', '%Y-%b', '%Y-%b-%d', '%Y-%b-%d', '%Y-%b-%d %H:%M'] 688 689 show_offset : bool, default: True 690 Whether to show the offset or not. 691 692 usetex : bool, default: :rc:`text.usetex` 693 To enable/disable the use of TeX's math mode for rendering the results 694 of the formatter. 695 696 Examples 697 -------- 698 See :doc:`/gallery/ticks_and_spines/date_concise_formatter` 699 700 .. plot:: 701 702 import datetime 703 import matplotlib.dates as mdates 704 705 base = datetime.datetime(2005, 2, 1) 706 dates = np.array([base + datetime.timedelta(hours=(2 * i)) 707 for i in range(732)]) 708 N = len(dates) 709 np.random.seed(19680801) 710 y = np.cumsum(np.random.randn(N)) 711 712 fig, ax = plt.subplots(constrained_layout=True) 713 locator = mdates.AutoDateLocator() 714 formatter = mdates.ConciseDateFormatter(locator) 715 ax.xaxis.set_major_locator(locator) 716 ax.xaxis.set_major_formatter(formatter) 717 718 ax.plot(dates, y) 719 ax.set_title('Concise Date Formatter') 720 721 """ 722 723 def __init__(self, locator, tz=None, formats=None, offset_formats=None, 724 zero_formats=None, show_offset=True, *, usetex=None): 725 """ 726 Autoformat the date labels. The default format is used to form an 727 initial string, and then redundant elements are removed. 728 """ 729 self._locator = locator 730 self._tz = tz 731 self.defaultfmt = '%Y' 732 # there are 6 levels with each level getting a specific format 733 # 0: mostly years, 1: months, 2: days, 734 # 3: hours, 4: minutes, 5: seconds 735 if formats: 736 if len(formats) != 6: 737 raise ValueError('formats argument must be a list of ' 738 '6 format strings (or None)') 739 self.formats = formats 740 else: 741 self.formats = ['%Y', # ticks are mostly years 742 '%b', # ticks are mostly months 743 '%d', # ticks are mostly days 744 '%H:%M', # hrs 745 '%H:%M', # min 746 '%S.%f', # secs 747 ] 748 # fmt for zeros ticks at this level. These are 749 # ticks that should be labeled w/ info the level above. 750 # like 1 Jan can just be labelled "Jan". 02:02:00 can 751 # just be labeled 02:02. 752 if zero_formats: 753 if len(zero_formats) != 6: 754 raise ValueError('zero_formats argument must be a list of ' 755 '6 format strings (or None)') 756 self.zero_formats = zero_formats 757 elif formats: 758 # use the users formats for the zero tick formats 759 self.zero_formats = [''] + self.formats[:-1] 760 else: 761 # make the defaults a bit nicer: 762 self.zero_formats = [''] + self.formats[:-1] 763 self.zero_formats[3] = '%b-%d' 764 765 if offset_formats: 766 if len(offset_formats) != 6: 767 raise ValueError('offsetfmts argument must be a list of ' 768 '6 format strings (or None)') 769 self.offset_formats = offset_formats 770 else: 771 self.offset_formats = ['', 772 '%Y', 773 '%Y-%b', 774 '%Y-%b-%d', 775 '%Y-%b-%d', 776 '%Y-%b-%d %H:%M'] 777 self.offset_string = '' 778 self.show_offset = show_offset 779 self._usetex = (usetex if usetex is not None else 780 mpl.rcParams['text.usetex']) 781 782 def __call__(self, x, pos=None): 783 formatter = DateFormatter(self.defaultfmt, self._tz, 784 usetex=self._usetex) 785 return formatter(x, pos=pos) 786 787 def format_ticks(self, values): 788 tickdatetime = [num2date(value, tz=self._tz) for value in values] 789 tickdate = np.array([tdt.timetuple()[:6] for tdt in tickdatetime]) 790 791 # basic algorithm: 792 # 1) only display a part of the date if it changes over the ticks. 793 # 2) don't display the smaller part of the date if: 794 # it is always the same or if it is the start of the 795 # year, month, day etc. 796 # fmt for most ticks at this level 797 fmts = self.formats 798 # format beginnings of days, months, years, etc... 799 zerofmts = self.zero_formats 800 # offset fmt are for the offset in the upper left of the 801 # or lower right of the axis. 802 offsetfmts = self.offset_formats 803 804 # determine the level we will label at: 805 # mostly 0: years, 1: months, 2: days, 806 # 3: hours, 4: minutes, 5: seconds, 6: microseconds 807 for level in range(5, -1, -1): 808 if len(np.unique(tickdate[:, level])) > 1: 809 # level is less than 2 so a year is already present in the axis 810 if (level < 2): 811 self.show_offset = False 812 break 813 elif level == 0: 814 # all tickdate are the same, so only micros might be different 815 # set to the most precise (6: microseconds doesn't exist...) 816 level = 5 817 818 # level is the basic level we will label at. 819 # now loop through and decide the actual ticklabels 820 zerovals = [0, 1, 1, 0, 0, 0, 0] 821 labels = [''] * len(tickdate) 822 for nn in range(len(tickdate)): 823 if level < 5: 824 if tickdate[nn][level] == zerovals[level]: 825 fmt = zerofmts[level] 826 else: 827 fmt = fmts[level] 828 else: 829 # special handling for seconds + microseconds 830 if (tickdatetime[nn].second == tickdatetime[nn].microsecond 831 == 0): 832 fmt = zerofmts[level] 833 else: 834 fmt = fmts[level] 835 labels[nn] = tickdatetime[nn].strftime(fmt) 836 837 # special handling of seconds and microseconds: 838 # strip extra zeros and decimal if possible. 839 # this is complicated by two factors. 1) we have some level-4 strings 840 # here (i.e. 03:00, '0.50000', '1.000') 2) we would like to have the 841 # same number of decimals for each string (i.e. 0.5 and 1.0). 842 if level >= 5: 843 trailing_zeros = min( 844 (len(s) - len(s.rstrip('0')) for s in labels if '.' in s), 845 default=None) 846 if trailing_zeros: 847 for nn in range(len(labels)): 848 if '.' in labels[nn]: 849 labels[nn] = labels[nn][:-trailing_zeros].rstrip('.') 850 851 if self.show_offset: 852 # set the offset string: 853 self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) 854 if self._usetex: 855 self.offset_string = _wrap_in_tex(self.offset_string) 856 857 if self._usetex: 858 return [_wrap_in_tex(l) for l in labels] 859 else: 860 return labels 861 862 def get_offset(self): 863 return self.offset_string 864 865 def format_data_short(self, value): 866 return num2date(value, tz=self._tz).strftime('%Y-%m-%d %H:%M:%S') 867 868 869class AutoDateFormatter(ticker.Formatter): 870 """ 871 A `.Formatter` which attempts to figure out the best format to use. This 872 is most useful when used with the `AutoDateLocator`. 873 874 The AutoDateFormatter has a scale dictionary that maps the scale 875 of the tick (the distance in days between one major tick) and a 876 format string. The default looks like this:: 877 878 self.scaled = { 879 DAYS_PER_YEAR: rcParams['date.autoformat.year'], 880 DAYS_PER_MONTH: rcParams['date.autoformat.month'], 881 1.0: rcParams['date.autoformat.day'], 882 1. / HOURS_PER_DAY: rcParams['date.autoformat.hour'], 883 1. / (MINUTES_PER_DAY): rcParams['date.autoformat.minute'], 884 1. / (SEC_PER_DAY): rcParams['date.autoformat.second'], 885 1. / (MUSECONDS_PER_DAY): rcParams['date.autoformat.microsecond'], 886 } 887 888 The algorithm picks the key in the dictionary that is >= the 889 current scale and uses that format string. You can customize this 890 dictionary by doing:: 891 892 >>> locator = AutoDateLocator() 893 >>> formatter = AutoDateFormatter(locator) 894 >>> formatter.scaled[1/(24.*60.)] = '%M:%S' # only show min and sec 895 896 A custom `.FuncFormatter` can also be used. The following example shows 897 how to use a custom format function to strip trailing zeros from decimal 898 seconds and adds the date to the first ticklabel:: 899 900 >>> def my_format_function(x, pos=None): 901 ... x = matplotlib.dates.num2date(x) 902 ... if pos == 0: 903 ... fmt = '%D %H:%M:%S.%f' 904 ... else: 905 ... fmt = '%H:%M:%S.%f' 906 ... label = x.strftime(fmt) 907 ... label = label.rstrip("0") 908 ... label = label.rstrip(".") 909 ... return label 910 >>> from matplotlib.ticker import FuncFormatter 911 >>> formatter.scaled[1/(24.*60.)] = FuncFormatter(my_format_function) 912 """ 913 914 # This can be improved by providing some user-level direction on 915 # how to choose the best format (precedence, etc...) 916 917 # Perhaps a 'struct' that has a field for each time-type where a 918 # zero would indicate "don't show" and a number would indicate 919 # "show" with some sort of priority. Same priorities could mean 920 # show all with the same priority. 921 922 # Or more simply, perhaps just a format string for each 923 # possibility... 924 925 def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *, 926 usetex=None): 927 """ 928 Autoformat the date labels. 929 930 Parameters 931 ---------- 932 locator : `.ticker.Locator` 933 Locator that this axis is using. 934 935 tz : str, optional 936 Passed to `.dates.date2num`. 937 938 defaultfmt : str 939 The default format to use if none of the values in ``self.scaled`` 940 are greater than the unit returned by ``locator._get_unit()``. 941 942 usetex : bool, default: :rc:`text.usetex` 943 To enable/disable the use of TeX's math mode for rendering the 944 results of the formatter. If any entries in ``self.scaled`` are set 945 as functions, then it is up to the customized function to enable or 946 disable TeX's math mode itself. 947 """ 948 self._locator = locator 949 self._tz = tz 950 self.defaultfmt = defaultfmt 951 self._formatter = DateFormatter(self.defaultfmt, tz) 952 rcParams = mpl.rcParams 953 self._usetex = (usetex if usetex is not None else 954 mpl.rcParams['text.usetex']) 955 self.scaled = { 956 DAYS_PER_YEAR: rcParams['date.autoformatter.year'], 957 DAYS_PER_MONTH: rcParams['date.autoformatter.month'], 958 1: rcParams['date.autoformatter.day'], 959 1 / HOURS_PER_DAY: rcParams['date.autoformatter.hour'], 960 1 / MINUTES_PER_DAY: rcParams['date.autoformatter.minute'], 961 1 / SEC_PER_DAY: rcParams['date.autoformatter.second'], 962 1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond'] 963 } 964 965 def _set_locator(self, locator): 966 self._locator = locator 967 968 def __call__(self, x, pos=None): 969 try: 970 locator_unit_scale = float(self._locator._get_unit()) 971 except AttributeError: 972 locator_unit_scale = 1 973 # Pick the first scale which is greater than the locator unit. 974 fmt = next((fmt for scale, fmt in sorted(self.scaled.items()) 975 if scale >= locator_unit_scale), 976 self.defaultfmt) 977 978 if isinstance(fmt, str): 979 self._formatter = DateFormatter(fmt, self._tz, usetex=self._usetex) 980 result = self._formatter(x, pos) 981 elif callable(fmt): 982 result = fmt(x, pos) 983 else: 984 raise TypeError('Unexpected type passed to {0!r}.'.format(self)) 985 986 return result 987 988 989class rrulewrapper: 990 def __init__(self, freq, tzinfo=None, **kwargs): 991 kwargs['freq'] = freq 992 self._base_tzinfo = tzinfo 993 994 self._update_rrule(**kwargs) 995 996 def set(self, **kwargs): 997 self._construct.update(kwargs) 998 999 self._update_rrule(**self._construct) 1000 1001 def _update_rrule(self, **kwargs): 1002 tzinfo = self._base_tzinfo 1003 1004 # rrule does not play nicely with time zones - especially pytz time 1005 # zones, it's best to use naive zones and attach timezones once the 1006 # datetimes are returned 1007 if 'dtstart' in kwargs: 1008 dtstart = kwargs['dtstart'] 1009 if dtstart.tzinfo is not None: 1010 if tzinfo is None: 1011 tzinfo = dtstart.tzinfo 1012 else: 1013 dtstart = dtstart.astimezone(tzinfo) 1014 1015 kwargs['dtstart'] = dtstart.replace(tzinfo=None) 1016 1017 if 'until' in kwargs: 1018 until = kwargs['until'] 1019 if until.tzinfo is not None: 1020 if tzinfo is not None: 1021 until = until.astimezone(tzinfo) 1022 else: 1023 raise ValueError('until cannot be aware if dtstart ' 1024 'is naive and tzinfo is None') 1025 1026 kwargs['until'] = until.replace(tzinfo=None) 1027 1028 self._construct = kwargs.copy() 1029 self._tzinfo = tzinfo 1030 self._rrule = rrule(**self._construct) 1031 1032 def _attach_tzinfo(self, dt, tzinfo): 1033 # pytz zones are attached by "localizing" the datetime 1034 if hasattr(tzinfo, 'localize'): 1035 return tzinfo.localize(dt, is_dst=True) 1036 1037 return dt.replace(tzinfo=tzinfo) 1038 1039 def _aware_return_wrapper(self, f, returns_list=False): 1040 """Decorator function that allows rrule methods to handle tzinfo.""" 1041 # This is only necessary if we're actually attaching a tzinfo 1042 if self._tzinfo is None: 1043 return f 1044 1045 # All datetime arguments must be naive. If they are not naive, they are 1046 # converted to the _tzinfo zone before dropping the zone. 1047 def normalize_arg(arg): 1048 if isinstance(arg, datetime.datetime) and arg.tzinfo is not None: 1049 if arg.tzinfo is not self._tzinfo: 1050 arg = arg.astimezone(self._tzinfo) 1051 1052 return arg.replace(tzinfo=None) 1053 1054 return arg 1055 1056 def normalize_args(args, kwargs): 1057 args = tuple(normalize_arg(arg) for arg in args) 1058 kwargs = {kw: normalize_arg(arg) for kw, arg in kwargs.items()} 1059 1060 return args, kwargs 1061 1062 # There are two kinds of functions we care about - ones that return 1063 # dates and ones that return lists of dates. 1064 if not returns_list: 1065 def inner_func(*args, **kwargs): 1066 args, kwargs = normalize_args(args, kwargs) 1067 dt = f(*args, **kwargs) 1068 return self._attach_tzinfo(dt, self._tzinfo) 1069 else: 1070 def inner_func(*args, **kwargs): 1071 args, kwargs = normalize_args(args, kwargs) 1072 dts = f(*args, **kwargs) 1073 return [self._attach_tzinfo(dt, self._tzinfo) for dt in dts] 1074 1075 return functools.wraps(f)(inner_func) 1076 1077 def __getattr__(self, name): 1078 if name in self.__dict__: 1079 return self.__dict__[name] 1080 1081 f = getattr(self._rrule, name) 1082 1083 if name in {'after', 'before'}: 1084 return self._aware_return_wrapper(f) 1085 elif name in {'xafter', 'xbefore', 'between'}: 1086 return self._aware_return_wrapper(f, returns_list=True) 1087 else: 1088 return f 1089 1090 def __setstate__(self, state): 1091 self.__dict__.update(state) 1092 1093 1094class DateLocator(ticker.Locator): 1095 """ 1096 Determines the tick locations when plotting dates. 1097 1098 This class is subclassed by other Locators and 1099 is not meant to be used on its own. 1100 """ 1101 hms0d = {'byhour': 0, 'byminute': 0, 'bysecond': 0} 1102 1103 def __init__(self, tz=None): 1104 """ 1105 Parameters 1106 ---------- 1107 tz : `datetime.tzinfo` 1108 """ 1109 if tz is None: 1110 tz = _get_rc_timezone() 1111 self.tz = tz 1112 1113 def set_tzinfo(self, tz): 1114 """ 1115 Set time zone info. 1116 """ 1117 self.tz = tz 1118 1119 def datalim_to_dt(self): 1120 """Convert axis data interval to datetime objects.""" 1121 dmin, dmax = self.axis.get_data_interval() 1122 if dmin > dmax: 1123 dmin, dmax = dmax, dmin 1124 1125 return num2date(dmin, self.tz), num2date(dmax, self.tz) 1126 1127 def viewlim_to_dt(self): 1128 """Convert the view interval to datetime objects.""" 1129 vmin, vmax = self.axis.get_view_interval() 1130 if vmin > vmax: 1131 vmin, vmax = vmax, vmin 1132 return num2date(vmin, self.tz), num2date(vmax, self.tz) 1133 1134 def _get_unit(self): 1135 """ 1136 Return how many days a unit of the locator is; used for 1137 intelligent autoscaling. 1138 """ 1139 return 1 1140 1141 def _get_interval(self): 1142 """ 1143 Return the number of units for each tick. 1144 """ 1145 return 1 1146 1147 def nonsingular(self, vmin, vmax): 1148 """ 1149 Given the proposed upper and lower extent, adjust the range 1150 if it is too close to being singular (i.e. a range of ~0). 1151 """ 1152 if not np.isfinite(vmin) or not np.isfinite(vmax): 1153 # Except if there is no data, then use 2000-2010 as default. 1154 return (date2num(datetime.date(2000, 1, 1)), 1155 date2num(datetime.date(2010, 1, 1))) 1156 if vmax < vmin: 1157 vmin, vmax = vmax, vmin 1158 unit = self._get_unit() 1159 interval = self._get_interval() 1160 if abs(vmax - vmin) < 1e-6: 1161 vmin -= 2 * unit * interval 1162 vmax += 2 * unit * interval 1163 return vmin, vmax 1164 1165 1166class RRuleLocator(DateLocator): 1167 # use the dateutil rrule instance 1168 1169 def __init__(self, o, tz=None): 1170 super().__init__(tz) 1171 self.rule = o 1172 1173 def __call__(self): 1174 # if no data have been set, this will tank with a ValueError 1175 try: 1176 dmin, dmax = self.viewlim_to_dt() 1177 except ValueError: 1178 return [] 1179 1180 return self.tick_values(dmin, dmax) 1181 1182 def tick_values(self, vmin, vmax): 1183 delta = relativedelta(vmax, vmin) 1184 1185 # We need to cap at the endpoints of valid datetime 1186 try: 1187 start = vmin - delta 1188 except (ValueError, OverflowError): 1189 # cap 1190 start = datetime.datetime(1, 1, 1, 0, 0, 0, 1191 tzinfo=datetime.timezone.utc) 1192 1193 try: 1194 stop = vmax + delta 1195 except (ValueError, OverflowError): 1196 # cap 1197 stop = datetime.datetime(9999, 12, 31, 23, 59, 59, 1198 tzinfo=datetime.timezone.utc) 1199 1200 self.rule.set(dtstart=start, until=stop) 1201 1202 dates = self.rule.between(vmin, vmax, True) 1203 if len(dates) == 0: 1204 return date2num([vmin, vmax]) 1205 return self.raise_if_exceeds(date2num(dates)) 1206 1207 def _get_unit(self): 1208 # docstring inherited 1209 freq = self.rule._rrule._freq 1210 return self.get_unit_generic(freq) 1211 1212 @staticmethod 1213 def get_unit_generic(freq): 1214 if freq == YEARLY: 1215 return DAYS_PER_YEAR 1216 elif freq == MONTHLY: 1217 return DAYS_PER_MONTH 1218 elif freq == WEEKLY: 1219 return DAYS_PER_WEEK 1220 elif freq == DAILY: 1221 return 1.0 1222 elif freq == HOURLY: 1223 return 1.0 / HOURS_PER_DAY 1224 elif freq == MINUTELY: 1225 return 1.0 / MINUTES_PER_DAY 1226 elif freq == SECONDLY: 1227 return 1.0 / SEC_PER_DAY 1228 else: 1229 # error 1230 return -1 # or should this just return '1'? 1231 1232 def _get_interval(self): 1233 return self.rule._rrule._interval 1234 1235 1236class AutoDateLocator(DateLocator): 1237 """ 1238 On autoscale, this class picks the best `DateLocator` to set the view 1239 limits and the tick locations. 1240 1241 Attributes 1242 ---------- 1243 intervald : dict 1244 1245 Mapping of tick frequencies to multiples allowed for that ticking. 1246 The default is :: 1247 1248 self.intervald = { 1249 YEARLY : [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1250 1000, 2000, 4000, 5000, 10000], 1251 MONTHLY : [1, 2, 3, 4, 6], 1252 DAILY : [1, 2, 3, 7, 14, 21], 1253 HOURLY : [1, 2, 3, 4, 6, 12], 1254 MINUTELY: [1, 5, 10, 15, 30], 1255 SECONDLY: [1, 5, 10, 15, 30], 1256 MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1257 1000, 2000, 5000, 10000, 20000, 50000, 1258 100000, 200000, 500000, 1000000], 1259 } 1260 1261 where the keys are defined in `dateutil.rrule`. 1262 1263 The interval is used to specify multiples that are appropriate for 1264 the frequency of ticking. For instance, every 7 days is sensible 1265 for daily ticks, but for minutes/seconds, 15 or 30 make sense. 1266 1267 When customizing, you should only modify the values for the existing 1268 keys. You should not add or delete entries. 1269 1270 Example for forcing ticks every 3 hours:: 1271 1272 locator = AutoDateLocator() 1273 locator.intervald[HOURLY] = [3] # only show every 3 hours 1274 """ 1275 1276 def __init__(self, tz=None, minticks=5, maxticks=None, 1277 interval_multiples=True): 1278 """ 1279 Parameters 1280 ---------- 1281 tz : `datetime.tzinfo` 1282 Ticks timezone. 1283 minticks : int 1284 The minimum number of ticks desired; controls whether ticks occur 1285 yearly, monthly, etc. 1286 maxticks : int 1287 The maximum number of ticks desired; controls the interval between 1288 ticks (ticking every other, every 3, etc.). For fine-grained 1289 control, this can be a dictionary mapping individual rrule 1290 frequency constants (YEARLY, MONTHLY, etc.) to their own maximum 1291 number of ticks. This can be used to keep the number of ticks 1292 appropriate to the format chosen in `AutoDateFormatter`. Any 1293 frequency not specified in this dictionary is given a default 1294 value. 1295 interval_multiples : bool, default: True 1296 Whether ticks should be chosen to be multiple of the interval, 1297 locking them to 'nicer' locations. For example, this will force 1298 the ticks to be at hours 0, 6, 12, 18 when hourly ticking is done 1299 at 6 hour intervals. 1300 """ 1301 super().__init__(tz) 1302 self._freq = YEARLY 1303 self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY, 1304 SECONDLY, MICROSECONDLY] 1305 self.minticks = minticks 1306 1307 self.maxticks = {YEARLY: 11, MONTHLY: 12, DAILY: 11, HOURLY: 12, 1308 MINUTELY: 11, SECONDLY: 11, MICROSECONDLY: 8} 1309 if maxticks is not None: 1310 try: 1311 self.maxticks.update(maxticks) 1312 except TypeError: 1313 # Assume we were given an integer. Use this as the maximum 1314 # number of ticks for every frequency and create a 1315 # dictionary for this 1316 self.maxticks = dict.fromkeys(self._freqs, maxticks) 1317 self.interval_multiples = interval_multiples 1318 self.intervald = { 1319 YEARLY: [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500, 1320 1000, 2000, 4000, 5000, 10000], 1321 MONTHLY: [1, 2, 3, 4, 6], 1322 DAILY: [1, 2, 3, 7, 14, 21], 1323 HOURLY: [1, 2, 3, 4, 6, 12], 1324 MINUTELY: [1, 5, 10, 15, 30], 1325 SECONDLY: [1, 5, 10, 15, 30], 1326 MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 1327 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1328 1000000], 1329 } 1330 if interval_multiples: 1331 # Swap "3" for "4" in the DAILY list; If we use 3 we get bad 1332 # tick loc for months w/ 31 days: 1, 4, ..., 28, 31, 1 1333 # If we use 4 then we get: 1, 5, ... 25, 29, 1 1334 self.intervald[DAILY] = [1, 2, 4, 7, 14] 1335 1336 self._byranges = [None, range(1, 13), range(1, 32), 1337 range(0, 24), range(0, 60), range(0, 60), None] 1338 1339 def __call__(self): 1340 # docstring inherited 1341 dmin, dmax = self.viewlim_to_dt() 1342 locator = self.get_locator(dmin, dmax) 1343 return locator() 1344 1345 def tick_values(self, vmin, vmax): 1346 return self.get_locator(vmin, vmax).tick_values(vmin, vmax) 1347 1348 def nonsingular(self, vmin, vmax): 1349 # whatever is thrown at us, we can scale the unit. 1350 # But default nonsingular date plots at an ~4 year period. 1351 if not np.isfinite(vmin) or not np.isfinite(vmax): 1352 # Except if there is no data, then use 2000-2010 as default. 1353 return (date2num(datetime.date(2000, 1, 1)), 1354 date2num(datetime.date(2010, 1, 1))) 1355 if vmax < vmin: 1356 vmin, vmax = vmax, vmin 1357 if vmin == vmax: 1358 vmin = vmin - DAYS_PER_YEAR * 2 1359 vmax = vmax + DAYS_PER_YEAR * 2 1360 return vmin, vmax 1361 1362 def _get_unit(self): 1363 if self._freq in [MICROSECONDLY]: 1364 return 1. / MUSECONDS_PER_DAY 1365 else: 1366 return RRuleLocator.get_unit_generic(self._freq) 1367 1368 def get_locator(self, dmin, dmax): 1369 """Pick the best locator based on a distance.""" 1370 delta = relativedelta(dmax, dmin) 1371 tdelta = dmax - dmin 1372 1373 # take absolute difference 1374 if dmin > dmax: 1375 delta = -delta 1376 tdelta = -tdelta 1377 # The following uses a mix of calls to relativedelta and timedelta 1378 # methods because there is incomplete overlap in the functionality of 1379 # these similar functions, and it's best to avoid doing our own math 1380 # whenever possible. 1381 numYears = float(delta.years) 1382 numMonths = numYears * MONTHS_PER_YEAR + delta.months 1383 numDays = tdelta.days # Avoids estimates of days/month, days/year 1384 numHours = numDays * HOURS_PER_DAY + delta.hours 1385 numMinutes = numHours * MIN_PER_HOUR + delta.minutes 1386 numSeconds = np.floor(tdelta.total_seconds()) 1387 numMicroseconds = np.floor(tdelta.total_seconds() * 1e6) 1388 1389 nums = [numYears, numMonths, numDays, numHours, numMinutes, 1390 numSeconds, numMicroseconds] 1391 1392 use_rrule_locator = [True] * 6 + [False] 1393 1394 # Default setting of bymonth, etc. to pass to rrule 1395 # [unused (for year), bymonth, bymonthday, byhour, byminute, 1396 # bysecond, unused (for microseconds)] 1397 byranges = [None, 1, 1, 0, 0, 0, None] 1398 1399 # Loop over all the frequencies and try to find one that gives at 1400 # least a minticks tick positions. Once this is found, look for 1401 # an interval from an list specific to that frequency that gives no 1402 # more than maxticks tick positions. Also, set up some ranges 1403 # (bymonth, etc.) as appropriate to be passed to rrulewrapper. 1404 for i, (freq, num) in enumerate(zip(self._freqs, nums)): 1405 # If this particular frequency doesn't give enough ticks, continue 1406 if num < self.minticks: 1407 # Since we're not using this particular frequency, set 1408 # the corresponding by_ to None so the rrule can act as 1409 # appropriate 1410 byranges[i] = None 1411 continue 1412 1413 # Find the first available interval that doesn't give too many 1414 # ticks 1415 for interval in self.intervald[freq]: 1416 if num <= interval * (self.maxticks[freq] - 1): 1417 break 1418 else: 1419 if not (self.interval_multiples and freq == DAILY): 1420 _api.warn_external( 1421 f"AutoDateLocator was unable to pick an appropriate " 1422 f"interval for this date range. It may be necessary " 1423 f"to add an interval value to the AutoDateLocator's " 1424 f"intervald dictionary. Defaulting to {interval}.") 1425 1426 # Set some parameters as appropriate 1427 self._freq = freq 1428 1429 if self._byranges[i] and self.interval_multiples: 1430 byranges[i] = self._byranges[i][::interval] 1431 if i in (DAILY, WEEKLY): 1432 if interval == 14: 1433 # just make first and 15th. Avoids 30th. 1434 byranges[i] = [1, 15] 1435 elif interval == 7: 1436 byranges[i] = [1, 8, 15, 22] 1437 1438 interval = 1 1439 else: 1440 byranges[i] = self._byranges[i] 1441 break 1442 else: 1443 interval = 1 1444 1445 if (freq == YEARLY) and self.interval_multiples: 1446 locator = YearLocator(interval, tz=self.tz) 1447 elif use_rrule_locator[i]: 1448 _, bymonth, bymonthday, byhour, byminute, bysecond, _ = byranges 1449 rrule = rrulewrapper(self._freq, interval=interval, 1450 dtstart=dmin, until=dmax, 1451 bymonth=bymonth, bymonthday=bymonthday, 1452 byhour=byhour, byminute=byminute, 1453 bysecond=bysecond) 1454 1455 locator = RRuleLocator(rrule, self.tz) 1456 else: 1457 locator = MicrosecondLocator(interval, tz=self.tz) 1458 if date2num(dmin) > 70 * 365 and interval < 1000: 1459 _api.warn_external( 1460 'Plotting microsecond time intervals for dates far from ' 1461 f'the epoch (time origin: {get_epoch()}) is not well-' 1462 'supported. See matplotlib.dates.set_epoch to change the ' 1463 'epoch.') 1464 1465 locator.set_axis(self.axis) 1466 1467 if self.axis is not None: 1468 locator.set_view_interval(*self.axis.get_view_interval()) 1469 locator.set_data_interval(*self.axis.get_data_interval()) 1470 return locator 1471 1472 1473class YearLocator(DateLocator): 1474 """ 1475 Make ticks on a given day of each year that is a multiple of base. 1476 1477 Examples:: 1478 1479 # Tick every year on Jan 1st 1480 locator = YearLocator() 1481 1482 # Tick every 5 years on July 4th 1483 locator = YearLocator(5, month=7, day=4) 1484 """ 1485 def __init__(self, base=1, month=1, day=1, tz=None): 1486 """ 1487 Mark years that are multiple of base on a given month and day 1488 (default jan 1). 1489 """ 1490 super().__init__(tz) 1491 self.base = ticker._Edge_integer(base, 0) 1492 self.replaced = {'month': month, 1493 'day': day, 1494 'hour': 0, 1495 'minute': 0, 1496 'second': 0, 1497 } 1498 if not hasattr(tz, 'localize'): 1499 # if tz is pytz, we need to do this w/ the localize fcn, 1500 # otherwise datetime.replace works fine... 1501 self.replaced['tzinfo'] = tz 1502 1503 def __call__(self): 1504 # if no data have been set, this will tank with a ValueError 1505 try: 1506 dmin, dmax = self.viewlim_to_dt() 1507 except ValueError: 1508 return [] 1509 1510 return self.tick_values(dmin, dmax) 1511 1512 def tick_values(self, vmin, vmax): 1513 ymin = self.base.le(vmin.year) * self.base.step 1514 ymax = self.base.ge(vmax.year) * self.base.step 1515 1516 vmin = vmin.replace(year=ymin, **self.replaced) 1517 if hasattr(self.tz, 'localize'): 1518 # look after pytz 1519 if not vmin.tzinfo: 1520 vmin = self.tz.localize(vmin, is_dst=True) 1521 1522 ticks = [vmin] 1523 1524 while True: 1525 dt = ticks[-1] 1526 if dt.year >= ymax: 1527 return date2num(ticks) 1528 year = dt.year + self.base.step 1529 dt = dt.replace(year=year, **self.replaced) 1530 if hasattr(self.tz, 'localize'): 1531 # look after pytz 1532 if not dt.tzinfo: 1533 dt = self.tz.localize(dt, is_dst=True) 1534 1535 ticks.append(dt) 1536 1537 1538class MonthLocator(RRuleLocator): 1539 """ 1540 Make ticks on occurrences of each month, e.g., 1, 3, 12. 1541 """ 1542 def __init__(self, bymonth=None, bymonthday=1, interval=1, tz=None): 1543 """ 1544 Mark every month in *bymonth*; *bymonth* can be an int or 1545 sequence. Default is ``range(1, 13)``, i.e. every month. 1546 1547 *interval* is the interval between each iteration. For 1548 example, if ``interval=2``, mark every second occurrence. 1549 """ 1550 if bymonth is None: 1551 bymonth = range(1, 13) 1552 elif isinstance(bymonth, np.ndarray): 1553 # This fixes a bug in dateutil <= 2.3 which prevents the use of 1554 # numpy arrays in (among other things) the bymonthday, byweekday 1555 # and bymonth parameters. 1556 bymonth = [x.item() for x in bymonth.astype(int)] 1557 1558 rule = rrulewrapper(MONTHLY, bymonth=bymonth, bymonthday=bymonthday, 1559 interval=interval, **self.hms0d) 1560 super().__init__(rule, tz) 1561 1562 1563class WeekdayLocator(RRuleLocator): 1564 """ 1565 Make ticks on occurrences of each weekday. 1566 """ 1567 1568 def __init__(self, byweekday=1, interval=1, tz=None): 1569 """ 1570 Mark every weekday in *byweekday*; *byweekday* can be a number or 1571 sequence. 1572 1573 Elements of *byweekday* must be one of MO, TU, WE, TH, FR, SA, 1574 SU, the constants from :mod:`dateutil.rrule`, which have been 1575 imported into the :mod:`matplotlib.dates` namespace. 1576 1577 *interval* specifies the number of weeks to skip. For example, 1578 ``interval=2`` plots every second week. 1579 """ 1580 if isinstance(byweekday, np.ndarray): 1581 # This fixes a bug in dateutil <= 2.3 which prevents the use of 1582 # numpy arrays in (among other things) the bymonthday, byweekday 1583 # and bymonth parameters. 1584 [x.item() for x in byweekday.astype(int)] 1585 1586 rule = rrulewrapper(DAILY, byweekday=byweekday, 1587 interval=interval, **self.hms0d) 1588 super().__init__(rule, tz) 1589 1590 1591class DayLocator(RRuleLocator): 1592 """ 1593 Make ticks on occurrences of each day of the month. For example, 1594 1, 15, 30. 1595 """ 1596 def __init__(self, bymonthday=None, interval=1, tz=None): 1597 """ 1598 Mark every day in *bymonthday*; *bymonthday* can be an int or sequence. 1599 1600 Default is to tick every day of the month: ``bymonthday=range(1, 32)``. 1601 """ 1602 if interval != int(interval) or interval < 1: 1603 raise ValueError("interval must be an integer greater than 0") 1604 if bymonthday is None: 1605 bymonthday = range(1, 32) 1606 elif isinstance(bymonthday, np.ndarray): 1607 # This fixes a bug in dateutil <= 2.3 which prevents the use of 1608 # numpy arrays in (among other things) the bymonthday, byweekday 1609 # and bymonth parameters. 1610 bymonthday = [x.item() for x in bymonthday.astype(int)] 1611 1612 rule = rrulewrapper(DAILY, bymonthday=bymonthday, 1613 interval=interval, **self.hms0d) 1614 super().__init__(rule, tz) 1615 1616 1617class HourLocator(RRuleLocator): 1618 """ 1619 Make ticks on occurrences of each hour. 1620 """ 1621 def __init__(self, byhour=None, interval=1, tz=None): 1622 """ 1623 Mark every hour in *byhour*; *byhour* can be an int or sequence. 1624 Default is to tick every hour: ``byhour=range(24)`` 1625 1626 *interval* is the interval between each iteration. For 1627 example, if ``interval=2``, mark every second occurrence. 1628 """ 1629 if byhour is None: 1630 byhour = range(24) 1631 1632 rule = rrulewrapper(HOURLY, byhour=byhour, interval=interval, 1633 byminute=0, bysecond=0) 1634 super().__init__(rule, tz) 1635 1636 1637class MinuteLocator(RRuleLocator): 1638 """ 1639 Make ticks on occurrences of each minute. 1640 """ 1641 def __init__(self, byminute=None, interval=1, tz=None): 1642 """ 1643 Mark every minute in *byminute*; *byminute* can be an int or 1644 sequence. Default is to tick every minute: ``byminute=range(60)`` 1645 1646 *interval* is the interval between each iteration. For 1647 example, if ``interval=2``, mark every second occurrence. 1648 """ 1649 if byminute is None: 1650 byminute = range(60) 1651 1652 rule = rrulewrapper(MINUTELY, byminute=byminute, interval=interval, 1653 bysecond=0) 1654 super().__init__(rule, tz) 1655 1656 1657class SecondLocator(RRuleLocator): 1658 """ 1659 Make ticks on occurrences of each second. 1660 """ 1661 def __init__(self, bysecond=None, interval=1, tz=None): 1662 """ 1663 Mark every second in *bysecond*; *bysecond* can be an int or 1664 sequence. Default is to tick every second: ``bysecond = range(60)`` 1665 1666 *interval* is the interval between each iteration. For 1667 example, if ``interval=2``, mark every second occurrence. 1668 1669 """ 1670 if bysecond is None: 1671 bysecond = range(60) 1672 1673 rule = rrulewrapper(SECONDLY, bysecond=bysecond, interval=interval) 1674 super().__init__(rule, tz) 1675 1676 1677class MicrosecondLocator(DateLocator): 1678 """ 1679 Make ticks on regular intervals of one or more microsecond(s). 1680 1681 .. note:: 1682 1683 By default, Matplotlib uses a floating point representation of time in 1684 days since the epoch, so plotting data with 1685 microsecond time resolution does not work well for 1686 dates that are far (about 70 years) from the epoch (check with 1687 `~.dates.get_epoch`). 1688 1689 If you want sub-microsecond resolution time plots, it is strongly 1690 recommended to use floating point seconds, not datetime-like 1691 time representation. 1692 1693 If you really must use datetime.datetime() or similar and still 1694 need microsecond precision, change the time origin via 1695 `.dates.set_epoch` to something closer to the dates being plotted. 1696 See :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. 1697 1698 """ 1699 def __init__(self, interval=1, tz=None): 1700 """ 1701 *interval* is the interval between each iteration. For 1702 example, if ``interval=2``, mark every second microsecond. 1703 1704 """ 1705 self._interval = interval 1706 self._wrapped_locator = ticker.MultipleLocator(interval) 1707 self.tz = tz 1708 1709 def set_axis(self, axis): 1710 self._wrapped_locator.set_axis(axis) 1711 return super().set_axis(axis) 1712 1713 def set_view_interval(self, vmin, vmax): 1714 self._wrapped_locator.set_view_interval(vmin, vmax) 1715 return super().set_view_interval(vmin, vmax) 1716 1717 def set_data_interval(self, vmin, vmax): 1718 self._wrapped_locator.set_data_interval(vmin, vmax) 1719 return super().set_data_interval(vmin, vmax) 1720 1721 def __call__(self): 1722 # if no data have been set, this will tank with a ValueError 1723 try: 1724 dmin, dmax = self.viewlim_to_dt() 1725 except ValueError: 1726 return [] 1727 1728 return self.tick_values(dmin, dmax) 1729 1730 def tick_values(self, vmin, vmax): 1731 nmin, nmax = date2num((vmin, vmax)) 1732 t0 = np.floor(nmin) 1733 nmax = nmax - t0 1734 nmin = nmin - t0 1735 nmin *= MUSECONDS_PER_DAY 1736 nmax *= MUSECONDS_PER_DAY 1737 1738 ticks = self._wrapped_locator.tick_values(nmin, nmax) 1739 1740 ticks = ticks / MUSECONDS_PER_DAY + t0 1741 return ticks 1742 1743 def _get_unit(self): 1744 # docstring inherited 1745 return 1. / MUSECONDS_PER_DAY 1746 1747 def _get_interval(self): 1748 # docstring inherited 1749 return self._interval 1750 1751 1752def epoch2num(e): 1753 """ 1754 Convert UNIX time to days since Matplotlib epoch. 1755 1756 Parameters 1757 ---------- 1758 e : list of floats 1759 Time in seconds since 1970-01-01. 1760 1761 Returns 1762 ------- 1763 `numpy.array` 1764 Time in days since Matplotlib epoch (see `~.dates.get_epoch()`). 1765 """ 1766 1767 dt = (np.datetime64('1970-01-01T00:00:00', 's') - 1768 np.datetime64(get_epoch(), 's')).astype(float) 1769 1770 return (dt + np.asarray(e)) / SEC_PER_DAY 1771 1772 1773def num2epoch(d): 1774 """ 1775 Convert days since Matplotlib epoch to UNIX time. 1776 1777 Parameters 1778 ---------- 1779 d : list of floats 1780 Time in days since Matplotlib epoch (see `~.dates.get_epoch()`). 1781 1782 Returns 1783 ------- 1784 `numpy.array` 1785 Time in seconds since 1970-01-01. 1786 """ 1787 dt = (np.datetime64('1970-01-01T00:00:00', 's') - 1788 np.datetime64(get_epoch(), 's')).astype(float) 1789 1790 return np.asarray(d) * SEC_PER_DAY - dt 1791 1792 1793def date_ticker_factory(span, tz=None, numticks=5): 1794 """ 1795 Create a date locator with *numticks* (approx) and a date formatter 1796 for *span* in days. Return value is (locator, formatter). 1797 """ 1798 1799 if span == 0: 1800 span = 1 / HOURS_PER_DAY 1801 1802 mins = span * MINUTES_PER_DAY 1803 hrs = span * HOURS_PER_DAY 1804 days = span 1805 wks = span / DAYS_PER_WEEK 1806 months = span / DAYS_PER_MONTH # Approx 1807 years = span / DAYS_PER_YEAR # Approx 1808 1809 if years > numticks: 1810 locator = YearLocator(int(years / numticks), tz=tz) # define 1811 fmt = '%Y' 1812 elif months > numticks: 1813 locator = MonthLocator(tz=tz) 1814 fmt = '%b %Y' 1815 elif wks > numticks: 1816 locator = WeekdayLocator(tz=tz) 1817 fmt = '%a, %b %d' 1818 elif days > numticks: 1819 locator = DayLocator(interval=math.ceil(days / numticks), tz=tz) 1820 fmt = '%b %d' 1821 elif hrs > numticks: 1822 locator = HourLocator(interval=math.ceil(hrs / numticks), tz=tz) 1823 fmt = '%H:%M\n%b %d' 1824 elif mins > numticks: 1825 locator = MinuteLocator(interval=math.ceil(mins / numticks), tz=tz) 1826 fmt = '%H:%M:%S' 1827 else: 1828 locator = MinuteLocator(tz=tz) 1829 fmt = '%H:%M:%S' 1830 1831 formatter = DateFormatter(fmt, tz=tz) 1832 return locator, formatter 1833 1834 1835class DateConverter(units.ConversionInterface): 1836 """ 1837 Converter for `datetime.date` and `datetime.datetime` data, or for 1838 date/time data represented as it would be converted by `date2num`. 1839 1840 The 'unit' tag for such data is None or a tzinfo instance. 1841 """ 1842 1843 def __init__(self, *, interval_multiples=True): 1844 self._interval_multiples = interval_multiples 1845 super().__init__() 1846 1847 def axisinfo(self, unit, axis): 1848 """ 1849 Return the `~matplotlib.units.AxisInfo` for *unit*. 1850 1851 *unit* is a tzinfo instance or None. 1852 The *axis* argument is required but not used. 1853 """ 1854 tz = unit 1855 1856 majloc = AutoDateLocator(tz=tz, 1857 interval_multiples=self._interval_multiples) 1858 majfmt = AutoDateFormatter(majloc, tz=tz) 1859 datemin = datetime.date(2000, 1, 1) 1860 datemax = datetime.date(2010, 1, 1) 1861 1862 return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', 1863 default_limits=(datemin, datemax)) 1864 1865 @staticmethod 1866 def convert(value, unit, axis): 1867 """ 1868 If *value* is not already a number or sequence of numbers, convert it 1869 with `date2num`. 1870 1871 The *unit* and *axis* arguments are not used. 1872 """ 1873 return date2num(value) 1874 1875 @staticmethod 1876 def default_units(x, axis): 1877 """ 1878 Return the tzinfo instance of *x* or of its first element, or None 1879 """ 1880 if isinstance(x, np.ndarray): 1881 x = x.ravel() 1882 1883 try: 1884 x = cbook.safe_first_element(x) 1885 except (TypeError, StopIteration): 1886 pass 1887 1888 try: 1889 return x.tzinfo 1890 except AttributeError: 1891 pass 1892 return None 1893 1894 1895class ConciseDateConverter(DateConverter): 1896 # docstring inherited 1897 1898 def __init__(self, formats=None, zero_formats=None, offset_formats=None, 1899 show_offset=True, *, interval_multiples=True): 1900 self._formats = formats 1901 self._zero_formats = zero_formats 1902 self._offset_formats = offset_formats 1903 self._show_offset = show_offset 1904 self._interval_multiples = interval_multiples 1905 super().__init__() 1906 1907 def axisinfo(self, unit, axis): 1908 # docstring inherited 1909 tz = unit 1910 majloc = AutoDateLocator(tz=tz, 1911 interval_multiples=self._interval_multiples) 1912 majfmt = ConciseDateFormatter(majloc, tz=tz, formats=self._formats, 1913 zero_formats=self._zero_formats, 1914 offset_formats=self._offset_formats, 1915 show_offset=self._show_offset) 1916 datemin = datetime.date(2000, 1, 1) 1917 datemax = datetime.date(2010, 1, 1) 1918 return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', 1919 default_limits=(datemin, datemax)) 1920 1921 1922class _rcParam_helper: 1923 """ 1924 This helper class is so that we can set the converter for dates 1925 via the validator for the rcParams `date.converter` and 1926 `date.interval_multiples`. Never instatiated. 1927 """ 1928 1929 conv_st = 'auto' 1930 int_mult = True 1931 1932 @classmethod 1933 def set_converter(cls, s): 1934 """Called by validator for rcParams date.converter""" 1935 if s not in ['concise', 'auto']: 1936 raise ValueError('Converter must be one of "concise" or "auto"') 1937 cls.conv_st = s 1938 cls.register_converters() 1939 1940 @classmethod 1941 def set_int_mult(cls, b): 1942 """Called by validator for rcParams date.interval_multiples""" 1943 cls.int_mult = b 1944 cls.register_converters() 1945 1946 @classmethod 1947 def register_converters(cls): 1948 """ 1949 Helper to register the date converters when rcParams `date.converter` 1950 and `date.interval_multiples` are changed. Called by the helpers 1951 above. 1952 """ 1953 if cls.conv_st == 'concise': 1954 converter = ConciseDateConverter 1955 else: 1956 converter = DateConverter 1957 1958 interval_multiples = cls.int_mult 1959 convert = converter(interval_multiples=interval_multiples) 1960 units.registry[np.datetime64] = convert 1961 units.registry[datetime.date] = convert 1962 units.registry[datetime.datetime] = convert 1963