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