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