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