1895 lines
66 KiB
Python
1895 lines
66 KiB
Python
"""
|
|
Matplotlib provides sophisticated date plotting capabilities, standing on the
|
|
shoulders of python :mod:`datetime` and the add-on module dateutil_.
|
|
|
|
By default, Matplotlib uses the units machinery described in
|
|
`~matplotlib.units` to convert `datetime.datetime`, and `numpy.datetime64`
|
|
objects when plotted on an x- or y-axis. The user does not
|
|
need to do anything for dates to be formatted, but dates often have strict
|
|
formatting needs, so this module provides many tick locators and formatters.
|
|
A basic example using `numpy.datetime64` is::
|
|
|
|
import numpy as np
|
|
|
|
times = np.arange(np.datetime64('2001-01-02'),
|
|
np.datetime64('2002-02-03'), np.timedelta64(75, 'm'))
|
|
y = np.random.randn(len(times))
|
|
|
|
fig, ax = plt.subplots()
|
|
ax.plot(times, y)
|
|
|
|
.. seealso::
|
|
|
|
- :doc:`/gallery/text_labels_and_annotations/date`
|
|
- :doc:`/gallery/ticks/date_concise_formatter`
|
|
- :doc:`/gallery/ticks/date_demo_convert`
|
|
|
|
.. _date-format:
|
|
|
|
Matplotlib date format
|
|
----------------------
|
|
|
|
Matplotlib represents dates using floating point numbers specifying the number
|
|
of days since a default epoch of 1970-01-01 UTC; for example,
|
|
1970-01-01, 06:00 is the floating point number 0.25. The formatters and
|
|
locators require the use of `datetime.datetime` objects, so only dates between
|
|
year 0001 and 9999 can be represented. Microsecond precision
|
|
is achievable for (approximately) 70 years on either side of the epoch, and
|
|
20 microseconds for the rest of the allowable range of dates (year 0001 to
|
|
9999). The epoch can be changed at import time via `.dates.set_epoch` or
|
|
:rc:`dates.epoch` to other dates if necessary; see
|
|
:doc:`/gallery/ticks/date_precision_and_epochs` for a discussion.
|
|
|
|
.. note::
|
|
|
|
Before Matplotlib 3.3, the epoch was 0000-12-31 which lost modern
|
|
microsecond precision and also made the default axis limit of 0 an invalid
|
|
datetime. In 3.3 the epoch was changed as above. To convert old
|
|
ordinal floats to the new epoch, users can do::
|
|
|
|
new_ordinal = old_ordinal + mdates.date2num(np.datetime64('0000-12-31'))
|
|
|
|
|
|
There are a number of helper functions to convert between :mod:`datetime`
|
|
objects and Matplotlib dates:
|
|
|
|
.. currentmodule:: matplotlib.dates
|
|
|
|
.. autosummary::
|
|
:nosignatures:
|
|
|
|
datestr2num
|
|
date2num
|
|
num2date
|
|
num2timedelta
|
|
drange
|
|
set_epoch
|
|
get_epoch
|
|
|
|
.. note::
|
|
|
|
Like Python's `datetime.datetime`, Matplotlib uses the Gregorian calendar
|
|
for all conversions between dates and floating point numbers. This practice
|
|
is not universal, and calendar differences can cause confusing
|
|
differences between what Python and Matplotlib give as the number of days
|
|
since 0001-01-01 and what other software and databases yield. For
|
|
example, the US Naval Observatory uses a calendar that switches
|
|
from Julian to Gregorian in October, 1582. Hence, using their
|
|
calculator, the number of days between 0001-01-01 and 2006-04-01 is
|
|
732403, whereas using the Gregorian calendar via the datetime
|
|
module we find::
|
|
|
|
In [1]: date(2006, 4, 1).toordinal() - date(1, 1, 1).toordinal()
|
|
Out[1]: 732401
|
|
|
|
All the Matplotlib date converters, locators and formatters are timezone aware.
|
|
If no explicit timezone is provided, :rc:`timezone` is assumed, provided as a
|
|
string. If you want to use a different timezone, pass the *tz* keyword
|
|
argument of `num2date` to any date tick locators or formatters you create. This
|
|
can be either a `datetime.tzinfo` instance or a string with the timezone name
|
|
that can be parsed by `~dateutil.tz.gettz`.
|
|
|
|
A wide range of specific and general purpose date tick locators and
|
|
formatters are provided in this module. See
|
|
:mod:`matplotlib.ticker` for general information on tick locators
|
|
and formatters. These are described below.
|
|
|
|
The dateutil_ module provides additional code to handle date ticking, making it
|
|
easy to place ticks on any kinds of dates. See examples below.
|
|
|
|
.. _dateutil: https://dateutil.readthedocs.io
|
|
|
|
.. _date-locators:
|
|
|
|
Date tick locators
|
|
------------------
|
|
|
|
Most of the date tick locators can locate single or multiple ticks. For example::
|
|
|
|
# import constants for the days of the week
|
|
from matplotlib.dates import MO, TU, WE, TH, FR, SA, SU
|
|
|
|
# tick on Mondays every week
|
|
loc = WeekdayLocator(byweekday=MO, tz=tz)
|
|
|
|
# tick on Mondays and Saturdays
|
|
loc = WeekdayLocator(byweekday=(MO, SA))
|
|
|
|
In addition, most of the constructors take an interval argument::
|
|
|
|
# tick on Mondays every second week
|
|
loc = WeekdayLocator(byweekday=MO, interval=2)
|
|
|
|
The rrule locator allows completely general date ticking::
|
|
|
|
# tick every 5th easter
|
|
rule = rrulewrapper(YEARLY, byeaster=1, interval=5)
|
|
loc = RRuleLocator(rule)
|
|
|
|
The available date tick locators are:
|
|
|
|
* `MicrosecondLocator`: Locate microseconds.
|
|
|
|
* `SecondLocator`: Locate seconds.
|
|
|
|
* `MinuteLocator`: Locate minutes.
|
|
|
|
* `HourLocator`: Locate hours.
|
|
|
|
* `DayLocator`: Locate specified days of the month.
|
|
|
|
* `WeekdayLocator`: Locate days of the week, e.g., MO, TU.
|
|
|
|
* `MonthLocator`: Locate months, e.g., 7 for July.
|
|
|
|
* `YearLocator`: Locate years that are multiples of base.
|
|
|
|
* `RRuleLocator`: Locate using a `rrulewrapper`.
|
|
`rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule`
|
|
which allow almost arbitrary date tick specifications.
|
|
See :doc:`rrule example </gallery/ticks/date_demo_rrule>`.
|
|
|
|
* `AutoDateLocator`: On autoscale, this class picks the best `DateLocator`
|
|
(e.g., `RRuleLocator`) to set the view limits and the tick locations. If
|
|
called with ``interval_multiples=True`` it will make ticks line up with
|
|
sensible multiples of the tick intervals. For example, if the interval is
|
|
4 hours, it will pick hours 0, 4, 8, etc. as ticks. This behaviour is not
|
|
guaranteed by default.
|
|
|
|
.. _date-formatters:
|
|
|
|
Date formatters
|
|
---------------
|
|
|
|
The available date formatters are:
|
|
|
|
* `AutoDateFormatter`: attempts to figure out the best format to use. This is
|
|
most useful when used with the `AutoDateLocator`.
|
|
|
|
* `ConciseDateFormatter`: also attempts to figure out the best format to use,
|
|
and to make the format as compact as possible while still having complete
|
|
date information. This is most useful when used with the `AutoDateLocator`.
|
|
|
|
* `DateFormatter`: use `~datetime.datetime.strftime` format strings.
|
|
"""
|
|
|
|
import datetime
|
|
import functools
|
|
import logging
|
|
import re
|
|
|
|
from dateutil.rrule import (rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY,
|
|
MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
|
|
SECONDLY)
|
|
from dateutil.relativedelta import relativedelta
|
|
import dateutil.parser
|
|
import dateutil.tz
|
|
import numpy as np
|
|
|
|
import matplotlib as mpl
|
|
from matplotlib import _api, cbook, ticker, units
|
|
|
|
__all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange',
|
|
'set_epoch', 'get_epoch', 'DateFormatter', 'ConciseDateFormatter',
|
|
'AutoDateFormatter', 'DateLocator', 'RRuleLocator',
|
|
'AutoDateLocator', 'YearLocator', 'MonthLocator', 'WeekdayLocator',
|
|
'DayLocator', 'HourLocator', 'MinuteLocator',
|
|
'SecondLocator', 'MicrosecondLocator',
|
|
'rrule', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU',
|
|
'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY',
|
|
'HOURLY', 'MINUTELY', 'SECONDLY', 'MICROSECONDLY', 'relativedelta',
|
|
'DateConverter', 'ConciseDateConverter', 'rrulewrapper')
|
|
|
|
|
|
_log = logging.getLogger(__name__)
|
|
UTC = datetime.timezone.utc
|
|
|
|
|
|
@_api.caching_module_getattr
|
|
class __getattr__:
|
|
JULIAN_OFFSET = _api.deprecated("3.7")(property(lambda self: 1721424.5))
|
|
# Julian date at 0000-12-31
|
|
# note that the Julian day epoch is achievable w/
|
|
# np.datetime64('-4713-11-24T12:00:00'); datetime64 is proleptic
|
|
# Gregorian and BC has a one-year offset. So
|
|
# np.datetime64('0000-12-31') - np.datetime64('-4713-11-24T12:00') =
|
|
# 1721424.5
|
|
# Ref: https://en.wikipedia.org/wiki/Julian_day
|
|
|
|
|
|
def _get_tzinfo(tz=None):
|
|
"""
|
|
Generate `~datetime.tzinfo` from a string or return `~datetime.tzinfo`.
|
|
If None, retrieve the preferred timezone from the rcParams dictionary.
|
|
"""
|
|
tz = mpl._val_or_rc(tz, 'timezone')
|
|
if tz == 'UTC':
|
|
return UTC
|
|
if isinstance(tz, str):
|
|
tzinfo = dateutil.tz.gettz(tz)
|
|
if tzinfo is None:
|
|
raise ValueError(f"{tz} is not a valid timezone as parsed by"
|
|
" dateutil.tz.gettz.")
|
|
return tzinfo
|
|
if isinstance(tz, datetime.tzinfo):
|
|
return tz
|
|
raise TypeError(f"tz must be string or tzinfo subclass, not {tz!r}.")
|
|
|
|
|
|
# Time-related constants.
|
|
EPOCH_OFFSET = float(datetime.datetime(1970, 1, 1).toordinal())
|
|
# EPOCH_OFFSET is not used by matplotlib
|
|
MICROSECONDLY = SECONDLY + 1
|
|
HOURS_PER_DAY = 24.
|
|
MIN_PER_HOUR = 60.
|
|
SEC_PER_MIN = 60.
|
|
MONTHS_PER_YEAR = 12.
|
|
|
|
DAYS_PER_WEEK = 7.
|
|
DAYS_PER_MONTH = 30.
|
|
DAYS_PER_YEAR = 365.0
|
|
|
|
MINUTES_PER_DAY = MIN_PER_HOUR * HOURS_PER_DAY
|
|
|
|
SEC_PER_HOUR = SEC_PER_MIN * MIN_PER_HOUR
|
|
SEC_PER_DAY = SEC_PER_HOUR * HOURS_PER_DAY
|
|
SEC_PER_WEEK = SEC_PER_DAY * DAYS_PER_WEEK
|
|
|
|
MUSECONDS_PER_DAY = 1e6 * SEC_PER_DAY
|
|
|
|
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = (
|
|
MO, TU, WE, TH, FR, SA, SU)
|
|
WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY)
|
|
|
|
# default epoch: passed to np.datetime64...
|
|
_epoch = None
|
|
|
|
|
|
def _reset_epoch_test_example():
|
|
"""
|
|
Reset the Matplotlib date epoch so it can be set again.
|
|
|
|
Only for use in tests and examples.
|
|
"""
|
|
global _epoch
|
|
_epoch = None
|
|
|
|
|
|
def set_epoch(epoch):
|
|
"""
|
|
Set the epoch (origin for dates) for datetime calculations.
|
|
|
|
The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00).
|
|
|
|
If microsecond accuracy is desired, the date being plotted needs to be
|
|
within approximately 70 years of the epoch. Matplotlib internally
|
|
represents dates as days since the epoch, so floating point dynamic
|
|
range needs to be within a factor of 2^52.
|
|
|
|
`~.dates.set_epoch` must be called before any dates are converted
|
|
(i.e. near the import section) or a RuntimeError will be raised.
|
|
|
|
See also :doc:`/gallery/ticks/date_precision_and_epochs`.
|
|
|
|
Parameters
|
|
----------
|
|
epoch : str
|
|
valid UTC date parsable by `numpy.datetime64` (do not include
|
|
timezone).
|
|
|
|
"""
|
|
global _epoch
|
|
if _epoch is not None:
|
|
raise RuntimeError('set_epoch must be called before dates plotted.')
|
|
_epoch = epoch
|
|
|
|
|
|
def get_epoch():
|
|
"""
|
|
Get the epoch used by `.dates`.
|
|
|
|
Returns
|
|
-------
|
|
epoch : str
|
|
String for the epoch (parsable by `numpy.datetime64`).
|
|
"""
|
|
global _epoch
|
|
|
|
_epoch = mpl._val_or_rc(_epoch, 'date.epoch')
|
|
return _epoch
|
|
|
|
|
|
def _dt64_to_ordinalf(d):
|
|
"""
|
|
Convert `numpy.datetime64` or an `numpy.ndarray` of those types to
|
|
Gregorian date as UTC float relative to the epoch (see `.get_epoch`).
|
|
Roundoff is float64 precision. Practically: microseconds for dates
|
|
between 290301 BC, 294241 AD, milliseconds for larger dates
|
|
(see `numpy.datetime64`).
|
|
"""
|
|
|
|
# the "extra" ensures that we at least allow the dynamic range out to
|
|
# seconds. That should get out to +/-2e11 years.
|
|
dseconds = d.astype('datetime64[s]')
|
|
extra = (d - dseconds).astype('timedelta64[ns]')
|
|
t0 = np.datetime64(get_epoch(), 's')
|
|
dt = (dseconds - t0).astype(np.float64)
|
|
dt += extra.astype(np.float64) / 1.0e9
|
|
dt = dt / SEC_PER_DAY
|
|
|
|
NaT_int = np.datetime64('NaT').astype(np.int64)
|
|
d_int = d.astype(np.int64)
|
|
dt[d_int == NaT_int] = np.nan
|
|
return dt
|
|
|
|
|
|
def _from_ordinalf(x, tz=None):
|
|
"""
|
|
Convert Gregorian float of the date, preserving hours, minutes,
|
|
seconds and microseconds. Return value is a `.datetime`.
|
|
|
|
The input date *x* is a float in ordinal days at UTC, and the output will
|
|
be the specified `.datetime` object corresponding to that time in
|
|
timezone *tz*, or if *tz* is ``None``, in the timezone specified in
|
|
:rc:`timezone`.
|
|
"""
|
|
|
|
tz = _get_tzinfo(tz)
|
|
|
|
dt = (np.datetime64(get_epoch()) +
|
|
np.timedelta64(int(np.round(x * MUSECONDS_PER_DAY)), 'us'))
|
|
if dt < np.datetime64('0001-01-01') or dt >= np.datetime64('10000-01-01'):
|
|
raise ValueError(f'Date ordinal {x} converts to {dt} (using '
|
|
f'epoch {get_epoch()}), but Matplotlib dates must be '
|
|
'between year 0001 and 9999.')
|
|
# convert from datetime64 to datetime:
|
|
dt = dt.tolist()
|
|
|
|
# datetime64 is always UTC:
|
|
dt = dt.replace(tzinfo=dateutil.tz.gettz('UTC'))
|
|
# but maybe we are working in a different timezone so move.
|
|
dt = dt.astimezone(tz)
|
|
# fix round off errors
|
|
if np.abs(x) > 70 * 365:
|
|
# if x is big, round off to nearest twenty microseconds.
|
|
# This avoids floating point roundoff error
|
|
ms = round(dt.microsecond / 20) * 20
|
|
if ms == 1000000:
|
|
dt = dt.replace(microsecond=0) + datetime.timedelta(seconds=1)
|
|
else:
|
|
dt = dt.replace(microsecond=ms)
|
|
|
|
return dt
|
|
|
|
|
|
# a version of _from_ordinalf that can operate on numpy arrays
|
|
_from_ordinalf_np_vectorized = np.vectorize(_from_ordinalf, otypes="O")
|
|
# a version of dateutil.parser.parse that can operate on numpy arrays
|
|
_dateutil_parser_parse_np_vectorized = np.vectorize(dateutil.parser.parse)
|
|
|
|
|
|
def datestr2num(d, default=None):
|
|
"""
|
|
Convert a date string to a datenum using `dateutil.parser.parse`.
|
|
|
|
Parameters
|
|
----------
|
|
d : str or sequence of str
|
|
The dates to convert.
|
|
|
|
default : datetime.datetime, optional
|
|
The default date to use when fields are missing in *d*.
|
|
"""
|
|
if isinstance(d, str):
|
|
dt = dateutil.parser.parse(d, default=default)
|
|
return date2num(dt)
|
|
else:
|
|
if default is not None:
|
|
d = [date2num(dateutil.parser.parse(s, default=default))
|
|
for s in d]
|
|
return np.asarray(d)
|
|
d = np.asarray(d)
|
|
if not d.size:
|
|
return d
|
|
return date2num(_dateutil_parser_parse_np_vectorized(d))
|
|
|
|
|
|
def date2num(d):
|
|
"""
|
|
Convert datetime objects to Matplotlib dates.
|
|
|
|
Parameters
|
|
----------
|
|
d : `datetime.datetime` or `numpy.datetime64` or sequences of these
|
|
|
|
Returns
|
|
-------
|
|
float or sequence of floats
|
|
Number of days since the epoch. See `.get_epoch` for the
|
|
epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`. If
|
|
the epoch is "1970-01-01T00:00:00" (default) then noon Jan 1 1970
|
|
("1970-01-01T12:00:00") returns 0.5.
|
|
|
|
Notes
|
|
-----
|
|
The Gregorian calendar is assumed; this is not universal practice.
|
|
For details see the module docstring.
|
|
"""
|
|
# Unpack in case of e.g. Pandas or xarray object
|
|
d = cbook._unpack_to_numpy(d)
|
|
|
|
# make an iterable, but save state to unpack later:
|
|
iterable = np.iterable(d)
|
|
if not iterable:
|
|
d = [d]
|
|
|
|
masked = np.ma.is_masked(d)
|
|
mask = np.ma.getmask(d)
|
|
d = np.asarray(d)
|
|
|
|
# convert to datetime64 arrays, if not already:
|
|
if not np.issubdtype(d.dtype, np.datetime64):
|
|
# datetime arrays
|
|
if not d.size:
|
|
# deals with an empty array...
|
|
return d
|
|
tzi = getattr(d[0], 'tzinfo', None)
|
|
if tzi is not None:
|
|
# make datetime naive:
|
|
d = [dt.astimezone(UTC).replace(tzinfo=None) for dt in d]
|
|
d = np.asarray(d)
|
|
d = d.astype('datetime64[us]')
|
|
|
|
d = np.ma.masked_array(d, mask=mask) if masked else d
|
|
d = _dt64_to_ordinalf(d)
|
|
|
|
return d if iterable else d[0]
|
|
|
|
|
|
@_api.deprecated("3.7")
|
|
def julian2num(j):
|
|
"""
|
|
Convert a Julian date (or sequence) to a Matplotlib date (or sequence).
|
|
|
|
Parameters
|
|
----------
|
|
j : float or sequence of floats
|
|
Julian dates (days relative to 4713 BC Jan 1, 12:00:00 Julian
|
|
calendar or 4714 BC Nov 24, 12:00:00, proleptic Gregorian calendar).
|
|
|
|
Returns
|
|
-------
|
|
float or sequence of floats
|
|
Matplotlib dates (days relative to `.get_epoch`).
|
|
"""
|
|
ep = np.datetime64(get_epoch(), 'h').astype(float) / 24.
|
|
ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24.
|
|
# Julian offset defined above is relative to 0000-12-31, but we need
|
|
# relative to our current epoch:
|
|
dt = __getattr__("JULIAN_OFFSET") - ep0 + ep
|
|
return np.subtract(j, dt) # Handles both scalar & nonscalar j.
|
|
|
|
|
|
@_api.deprecated("3.7")
|
|
def num2julian(n):
|
|
"""
|
|
Convert a Matplotlib date (or sequence) to a Julian date (or sequence).
|
|
|
|
Parameters
|
|
----------
|
|
n : float or sequence of floats
|
|
Matplotlib dates (days relative to `.get_epoch`).
|
|
|
|
Returns
|
|
-------
|
|
float or sequence of floats
|
|
Julian dates (days relative to 4713 BC Jan 1, 12:00:00).
|
|
"""
|
|
ep = np.datetime64(get_epoch(), 'h').astype(float) / 24.
|
|
ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24.
|
|
# Julian offset defined above is relative to 0000-12-31, but we need
|
|
# relative to our current epoch:
|
|
dt = __getattr__("JULIAN_OFFSET") - ep0 + ep
|
|
return np.add(n, dt) # Handles both scalar & nonscalar j.
|
|
|
|
|
|
def num2date(x, tz=None):
|
|
"""
|
|
Convert Matplotlib dates to `~datetime.datetime` objects.
|
|
|
|
Parameters
|
|
----------
|
|
x : float or sequence of floats
|
|
Number of days (fraction part represents hours, minutes, seconds)
|
|
since the epoch. See `.get_epoch` for the
|
|
epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Timezone of *x*. If a string, *tz* is passed to `dateutil.tz`.
|
|
|
|
Returns
|
|
-------
|
|
`~datetime.datetime` or sequence of `~datetime.datetime`
|
|
Dates are returned in timezone *tz*.
|
|
|
|
If *x* is a sequence, a sequence of `~datetime.datetime` objects will
|
|
be returned.
|
|
|
|
Notes
|
|
-----
|
|
The Gregorian calendar is assumed; this is not universal practice.
|
|
For details, see the module docstring.
|
|
"""
|
|
tz = _get_tzinfo(tz)
|
|
return _from_ordinalf_np_vectorized(x, tz).tolist()
|
|
|
|
|
|
_ordinalf_to_timedelta_np_vectorized = np.vectorize(
|
|
lambda x: datetime.timedelta(days=x), otypes="O")
|
|
|
|
|
|
def num2timedelta(x):
|
|
"""
|
|
Convert number of days to a `~datetime.timedelta` object.
|
|
|
|
If *x* is a sequence, a sequence of `~datetime.timedelta` objects will
|
|
be returned.
|
|
|
|
Parameters
|
|
----------
|
|
x : float, sequence of floats
|
|
Number of days. The fraction part represents hours, minutes, seconds.
|
|
|
|
Returns
|
|
-------
|
|
`datetime.timedelta` or list[`datetime.timedelta`]
|
|
"""
|
|
return _ordinalf_to_timedelta_np_vectorized(x).tolist()
|
|
|
|
|
|
def drange(dstart, dend, delta):
|
|
"""
|
|
Return a sequence of equally spaced Matplotlib dates.
|
|
|
|
The dates start at *dstart* and reach up to, but not including *dend*.
|
|
They are spaced by *delta*.
|
|
|
|
Parameters
|
|
----------
|
|
dstart, dend : `~datetime.datetime`
|
|
The date limits.
|
|
delta : `datetime.timedelta`
|
|
Spacing of the dates.
|
|
|
|
Returns
|
|
-------
|
|
`numpy.array`
|
|
A list floats representing Matplotlib dates.
|
|
|
|
"""
|
|
f1 = date2num(dstart)
|
|
f2 = date2num(dend)
|
|
step = delta.total_seconds() / SEC_PER_DAY
|
|
|
|
# calculate the difference between dend and dstart in times of delta
|
|
num = int(np.ceil((f2 - f1) / step))
|
|
|
|
# calculate end of the interval which will be generated
|
|
dinterval_end = dstart + num * delta
|
|
|
|
# ensure, that an half open interval will be generated [dstart, dend)
|
|
if dinterval_end >= dend:
|
|
# if the endpoint is greater than or equal to dend,
|
|
# just subtract one delta
|
|
dinterval_end -= delta
|
|
num -= 1
|
|
|
|
f2 = date2num(dinterval_end) # new float-endpoint
|
|
return np.linspace(f1, f2, num + 1)
|
|
|
|
|
|
def _wrap_in_tex(text):
|
|
p = r'([a-zA-Z]+)'
|
|
ret_text = re.sub(p, r'}$\1$\\mathdefault{', text)
|
|
|
|
# Braces ensure symbols are not spaced like binary operators.
|
|
ret_text = ret_text.replace('-', '{-}').replace(':', '{:}')
|
|
# To not concatenate space between numbers.
|
|
ret_text = ret_text.replace(' ', r'\;')
|
|
ret_text = '$\\mathdefault{' + ret_text + '}$'
|
|
ret_text = ret_text.replace('$\\mathdefault{}$', '')
|
|
return ret_text
|
|
|
|
|
|
## date tick locators and formatters ###
|
|
|
|
|
|
class DateFormatter(ticker.Formatter):
|
|
"""
|
|
Format a tick (in days since the epoch) with a
|
|
`~datetime.datetime.strftime` format string.
|
|
"""
|
|
|
|
def __init__(self, fmt, tz=None, *, usetex=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
fmt : str
|
|
`~datetime.datetime.strftime` format string
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
usetex : bool, default: :rc:`text.usetex`
|
|
To enable/disable the use of TeX's math mode for rendering the
|
|
results of the formatter.
|
|
"""
|
|
self.tz = _get_tzinfo(tz)
|
|
self.fmt = fmt
|
|
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
|
|
|
|
def __call__(self, x, pos=0):
|
|
result = num2date(x, self.tz).strftime(self.fmt)
|
|
return _wrap_in_tex(result) if self._usetex else result
|
|
|
|
def set_tzinfo(self, tz):
|
|
self.tz = _get_tzinfo(tz)
|
|
|
|
|
|
class ConciseDateFormatter(ticker.Formatter):
|
|
"""
|
|
A `.Formatter` which attempts to figure out the best format to use for the
|
|
date, and to make it as compact as possible, but still be complete. This is
|
|
most useful when used with the `AutoDateLocator`::
|
|
|
|
>>> locator = AutoDateLocator()
|
|
>>> formatter = ConciseDateFormatter(locator)
|
|
|
|
Parameters
|
|
----------
|
|
locator : `.ticker.Locator`
|
|
Locator that this axis is using.
|
|
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone, passed to `.dates.num2date`.
|
|
|
|
formats : list of 6 strings, optional
|
|
Format strings for 6 levels of tick labelling: mostly years,
|
|
months, days, hours, minutes, and seconds. Strings use
|
|
the same format codes as `~datetime.datetime.strftime`. Default is
|
|
``['%Y', '%b', '%d', '%H:%M', '%H:%M', '%S.%f']``
|
|
|
|
zero_formats : list of 6 strings, optional
|
|
Format strings for tick labels that are "zeros" for a given tick
|
|
level. For instance, if most ticks are months, ticks around 1 Jan 2005
|
|
will be labeled "Dec", "2005", "Feb". The default is
|
|
``['', '%Y', '%b', '%b-%d', '%H:%M', '%H:%M']``
|
|
|
|
offset_formats : list of 6 strings, optional
|
|
Format strings for the 6 levels that is applied to the "offset"
|
|
string found on the right side of an x-axis, or top of a y-axis.
|
|
Combined with the tick labels this should completely specify the
|
|
date. The default is::
|
|
|
|
['', '%Y', '%Y-%b', '%Y-%b-%d', '%Y-%b-%d', '%Y-%b-%d %H:%M']
|
|
|
|
show_offset : bool, default: True
|
|
Whether to show the offset or not.
|
|
|
|
usetex : bool, default: :rc:`text.usetex`
|
|
To enable/disable the use of TeX's math mode for rendering the results
|
|
of the formatter.
|
|
|
|
Examples
|
|
--------
|
|
See :doc:`/gallery/ticks/date_concise_formatter`
|
|
|
|
.. plot::
|
|
|
|
import datetime
|
|
import matplotlib.dates as mdates
|
|
|
|
base = datetime.datetime(2005, 2, 1)
|
|
dates = np.array([base + datetime.timedelta(hours=(2 * i))
|
|
for i in range(732)])
|
|
N = len(dates)
|
|
np.random.seed(19680801)
|
|
y = np.cumsum(np.random.randn(N))
|
|
|
|
fig, ax = plt.subplots(constrained_layout=True)
|
|
locator = mdates.AutoDateLocator()
|
|
formatter = mdates.ConciseDateFormatter(locator)
|
|
ax.xaxis.set_major_locator(locator)
|
|
ax.xaxis.set_major_formatter(formatter)
|
|
|
|
ax.plot(dates, y)
|
|
ax.set_title('Concise Date Formatter')
|
|
|
|
"""
|
|
|
|
def __init__(self, locator, tz=None, formats=None, offset_formats=None,
|
|
zero_formats=None, show_offset=True, *, usetex=None):
|
|
"""
|
|
Autoformat the date labels. The default format is used to form an
|
|
initial string, and then redundant elements are removed.
|
|
"""
|
|
self._locator = locator
|
|
self._tz = tz
|
|
self.defaultfmt = '%Y'
|
|
# there are 6 levels with each level getting a specific format
|
|
# 0: mostly years, 1: months, 2: days,
|
|
# 3: hours, 4: minutes, 5: seconds
|
|
if formats:
|
|
if len(formats) != 6:
|
|
raise ValueError('formats argument must be a list of '
|
|
'6 format strings (or None)')
|
|
self.formats = formats
|
|
else:
|
|
self.formats = ['%Y', # ticks are mostly years
|
|
'%b', # ticks are mostly months
|
|
'%d', # ticks are mostly days
|
|
'%H:%M', # hrs
|
|
'%H:%M', # min
|
|
'%S.%f', # secs
|
|
]
|
|
# fmt for zeros ticks at this level. These are
|
|
# ticks that should be labeled w/ info the level above.
|
|
# like 1 Jan can just be labelled "Jan". 02:02:00 can
|
|
# just be labeled 02:02.
|
|
if zero_formats:
|
|
if len(zero_formats) != 6:
|
|
raise ValueError('zero_formats argument must be a list of '
|
|
'6 format strings (or None)')
|
|
self.zero_formats = zero_formats
|
|
elif formats:
|
|
# use the users formats for the zero tick formats
|
|
self.zero_formats = [''] + self.formats[:-1]
|
|
else:
|
|
# make the defaults a bit nicer:
|
|
self.zero_formats = [''] + self.formats[:-1]
|
|
self.zero_formats[3] = '%b-%d'
|
|
|
|
if offset_formats:
|
|
if len(offset_formats) != 6:
|
|
raise ValueError('offset_formats argument must be a list of '
|
|
'6 format strings (or None)')
|
|
self.offset_formats = offset_formats
|
|
else:
|
|
self.offset_formats = ['',
|
|
'%Y',
|
|
'%Y-%b',
|
|
'%Y-%b-%d',
|
|
'%Y-%b-%d',
|
|
'%Y-%b-%d %H:%M']
|
|
self.offset_string = ''
|
|
self.show_offset = show_offset
|
|
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
|
|
|
|
def __call__(self, x, pos=None):
|
|
formatter = DateFormatter(self.defaultfmt, self._tz,
|
|
usetex=self._usetex)
|
|
return formatter(x, pos=pos)
|
|
|
|
def format_ticks(self, values):
|
|
tickdatetime = [num2date(value, tz=self._tz) for value in values]
|
|
tickdate = np.array([tdt.timetuple()[:6] for tdt in tickdatetime])
|
|
|
|
# basic algorithm:
|
|
# 1) only display a part of the date if it changes over the ticks.
|
|
# 2) don't display the smaller part of the date if:
|
|
# it is always the same or if it is the start of the
|
|
# year, month, day etc.
|
|
# fmt for most ticks at this level
|
|
fmts = self.formats
|
|
# format beginnings of days, months, years, etc.
|
|
zerofmts = self.zero_formats
|
|
# offset fmt are for the offset in the upper left of the
|
|
# or lower right of the axis.
|
|
offsetfmts = self.offset_formats
|
|
show_offset = self.show_offset
|
|
|
|
# determine the level we will label at:
|
|
# mostly 0: years, 1: months, 2: days,
|
|
# 3: hours, 4: minutes, 5: seconds, 6: microseconds
|
|
for level in range(5, -1, -1):
|
|
unique = np.unique(tickdate[:, level])
|
|
if len(unique) > 1:
|
|
# if 1 is included in unique, the year is shown in ticks
|
|
if level < 2 and np.any(unique == 1):
|
|
show_offset = False
|
|
break
|
|
elif level == 0:
|
|
# all tickdate are the same, so only micros might be different
|
|
# set to the most precise (6: microseconds doesn't exist...)
|
|
level = 5
|
|
|
|
# level is the basic level we will label at.
|
|
# now loop through and decide the actual ticklabels
|
|
zerovals = [0, 1, 1, 0, 0, 0, 0]
|
|
labels = [''] * len(tickdate)
|
|
for nn in range(len(tickdate)):
|
|
if level < 5:
|
|
if tickdate[nn][level] == zerovals[level]:
|
|
fmt = zerofmts[level]
|
|
else:
|
|
fmt = fmts[level]
|
|
else:
|
|
# special handling for seconds + microseconds
|
|
if (tickdatetime[nn].second == tickdatetime[nn].microsecond
|
|
== 0):
|
|
fmt = zerofmts[level]
|
|
else:
|
|
fmt = fmts[level]
|
|
labels[nn] = tickdatetime[nn].strftime(fmt)
|
|
|
|
# special handling of seconds and microseconds:
|
|
# strip extra zeros and decimal if possible.
|
|
# this is complicated by two factors. 1) we have some level-4 strings
|
|
# here (i.e. 03:00, '0.50000', '1.000') 2) we would like to have the
|
|
# same number of decimals for each string (i.e. 0.5 and 1.0).
|
|
if level >= 5:
|
|
trailing_zeros = min(
|
|
(len(s) - len(s.rstrip('0')) for s in labels if '.' in s),
|
|
default=None)
|
|
if trailing_zeros:
|
|
for nn in range(len(labels)):
|
|
if '.' in labels[nn]:
|
|
labels[nn] = labels[nn][:-trailing_zeros].rstrip('.')
|
|
|
|
if show_offset:
|
|
# set the offset string:
|
|
self.offset_string = tickdatetime[-1].strftime(offsetfmts[level])
|
|
if self._usetex:
|
|
self.offset_string = _wrap_in_tex(self.offset_string)
|
|
else:
|
|
self.offset_string = ''
|
|
|
|
if self._usetex:
|
|
return [_wrap_in_tex(l) for l in labels]
|
|
else:
|
|
return labels
|
|
|
|
def get_offset(self):
|
|
return self.offset_string
|
|
|
|
def format_data_short(self, value):
|
|
return num2date(value, tz=self._tz).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
|
|
class AutoDateFormatter(ticker.Formatter):
|
|
"""
|
|
A `.Formatter` which attempts to figure out the best format to use. This
|
|
is most useful when used with the `AutoDateLocator`.
|
|
|
|
`.AutoDateFormatter` has a ``.scale`` dictionary that maps tick scales (the
|
|
interval in days between one major tick) to format strings; this dictionary
|
|
defaults to ::
|
|
|
|
self.scaled = {
|
|
DAYS_PER_YEAR: rcParams['date.autoformatter.year'],
|
|
DAYS_PER_MONTH: rcParams['date.autoformatter.month'],
|
|
1: rcParams['date.autoformatter.day'],
|
|
1 / HOURS_PER_DAY: rcParams['date.autoformatter.hour'],
|
|
1 / MINUTES_PER_DAY: rcParams['date.autoformatter.minute'],
|
|
1 / SEC_PER_DAY: rcParams['date.autoformatter.second'],
|
|
1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond'],
|
|
}
|
|
|
|
The formatter uses the format string corresponding to the lowest key in
|
|
the dictionary that is greater or equal to the current scale. Dictionary
|
|
entries can be customized::
|
|
|
|
locator = AutoDateLocator()
|
|
formatter = AutoDateFormatter(locator)
|
|
formatter.scaled[1/(24*60)] = '%M:%S' # only show min and sec
|
|
|
|
Custom callables can also be used instead of format strings. The following
|
|
example shows how to use a custom format function to strip trailing zeros
|
|
from decimal seconds and adds the date to the first ticklabel::
|
|
|
|
def my_format_function(x, pos=None):
|
|
x = matplotlib.dates.num2date(x)
|
|
if pos == 0:
|
|
fmt = '%D %H:%M:%S.%f'
|
|
else:
|
|
fmt = '%H:%M:%S.%f'
|
|
label = x.strftime(fmt)
|
|
label = label.rstrip("0")
|
|
label = label.rstrip(".")
|
|
return label
|
|
|
|
formatter.scaled[1/(24*60)] = my_format_function
|
|
"""
|
|
|
|
# This can be improved by providing some user-level direction on
|
|
# how to choose the best format (precedence, etc.).
|
|
|
|
# Perhaps a 'struct' that has a field for each time-type where a
|
|
# zero would indicate "don't show" and a number would indicate
|
|
# "show" with some sort of priority. Same priorities could mean
|
|
# show all with the same priority.
|
|
|
|
# Or more simply, perhaps just a format string for each
|
|
# possibility...
|
|
|
|
def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
|
|
usetex=None):
|
|
"""
|
|
Autoformat the date labels.
|
|
|
|
Parameters
|
|
----------
|
|
locator : `.ticker.Locator`
|
|
Locator that this axis is using.
|
|
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
|
|
defaultfmt : str
|
|
The default format to use if none of the values in ``self.scaled``
|
|
are greater than the unit returned by ``locator._get_unit()``.
|
|
|
|
usetex : bool, default: :rc:`text.usetex`
|
|
To enable/disable the use of TeX's math mode for rendering the
|
|
results of the formatter. If any entries in ``self.scaled`` are set
|
|
as functions, then it is up to the customized function to enable or
|
|
disable TeX's math mode itself.
|
|
"""
|
|
self._locator = locator
|
|
self._tz = tz
|
|
self.defaultfmt = defaultfmt
|
|
self._formatter = DateFormatter(self.defaultfmt, tz)
|
|
rcParams = mpl.rcParams
|
|
self._usetex = mpl._val_or_rc(usetex, 'text.usetex')
|
|
self.scaled = {
|
|
DAYS_PER_YEAR: rcParams['date.autoformatter.year'],
|
|
DAYS_PER_MONTH: rcParams['date.autoformatter.month'],
|
|
1: rcParams['date.autoformatter.day'],
|
|
1 / HOURS_PER_DAY: rcParams['date.autoformatter.hour'],
|
|
1 / MINUTES_PER_DAY: rcParams['date.autoformatter.minute'],
|
|
1 / SEC_PER_DAY: rcParams['date.autoformatter.second'],
|
|
1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond']
|
|
}
|
|
|
|
def _set_locator(self, locator):
|
|
self._locator = locator
|
|
|
|
def __call__(self, x, pos=None):
|
|
try:
|
|
locator_unit_scale = float(self._locator._get_unit())
|
|
except AttributeError:
|
|
locator_unit_scale = 1
|
|
# Pick the first scale which is greater than the locator unit.
|
|
fmt = next((fmt for scale, fmt in sorted(self.scaled.items())
|
|
if scale >= locator_unit_scale),
|
|
self.defaultfmt)
|
|
|
|
if isinstance(fmt, str):
|
|
self._formatter = DateFormatter(fmt, self._tz, usetex=self._usetex)
|
|
result = self._formatter(x, pos)
|
|
elif callable(fmt):
|
|
result = fmt(x, pos)
|
|
else:
|
|
raise TypeError(f'Unexpected type passed to {self!r}.')
|
|
|
|
return result
|
|
|
|
|
|
class rrulewrapper:
|
|
"""
|
|
A simple wrapper around a `dateutil.rrule` allowing flexible
|
|
date tick specifications.
|
|
"""
|
|
def __init__(self, freq, tzinfo=None, **kwargs):
|
|
"""
|
|
Parameters
|
|
----------
|
|
freq : {YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY}
|
|
Tick frequency. These constants are defined in `dateutil.rrule`,
|
|
but they are accessible from `matplotlib.dates` as well.
|
|
tzinfo : `datetime.tzinfo`, optional
|
|
Time zone information. The default is None.
|
|
**kwargs
|
|
Additional keyword arguments are passed to the `dateutil.rrule`.
|
|
"""
|
|
kwargs['freq'] = freq
|
|
self._base_tzinfo = tzinfo
|
|
|
|
self._update_rrule(**kwargs)
|
|
|
|
def set(self, **kwargs):
|
|
"""Set parameters for an existing wrapper."""
|
|
self._construct.update(kwargs)
|
|
|
|
self._update_rrule(**self._construct)
|
|
|
|
def _update_rrule(self, **kwargs):
|
|
tzinfo = self._base_tzinfo
|
|
|
|
# rrule does not play nicely with timezones - especially pytz time
|
|
# zones, it's best to use naive zones and attach timezones once the
|
|
# datetimes are returned
|
|
if 'dtstart' in kwargs:
|
|
dtstart = kwargs['dtstart']
|
|
if dtstart.tzinfo is not None:
|
|
if tzinfo is None:
|
|
tzinfo = dtstart.tzinfo
|
|
else:
|
|
dtstart = dtstart.astimezone(tzinfo)
|
|
|
|
kwargs['dtstart'] = dtstart.replace(tzinfo=None)
|
|
|
|
if 'until' in kwargs:
|
|
until = kwargs['until']
|
|
if until.tzinfo is not None:
|
|
if tzinfo is not None:
|
|
until = until.astimezone(tzinfo)
|
|
else:
|
|
raise ValueError('until cannot be aware if dtstart '
|
|
'is naive and tzinfo is None')
|
|
|
|
kwargs['until'] = until.replace(tzinfo=None)
|
|
|
|
self._construct = kwargs.copy()
|
|
self._tzinfo = tzinfo
|
|
self._rrule = rrule(**self._construct)
|
|
|
|
def _attach_tzinfo(self, dt, tzinfo):
|
|
# pytz zones are attached by "localizing" the datetime
|
|
if hasattr(tzinfo, 'localize'):
|
|
return tzinfo.localize(dt, is_dst=True)
|
|
|
|
return dt.replace(tzinfo=tzinfo)
|
|
|
|
def _aware_return_wrapper(self, f, returns_list=False):
|
|
"""Decorator function that allows rrule methods to handle tzinfo."""
|
|
# This is only necessary if we're actually attaching a tzinfo
|
|
if self._tzinfo is None:
|
|
return f
|
|
|
|
# All datetime arguments must be naive. If they are not naive, they are
|
|
# converted to the _tzinfo zone before dropping the zone.
|
|
def normalize_arg(arg):
|
|
if isinstance(arg, datetime.datetime) and arg.tzinfo is not None:
|
|
if arg.tzinfo is not self._tzinfo:
|
|
arg = arg.astimezone(self._tzinfo)
|
|
|
|
return arg.replace(tzinfo=None)
|
|
|
|
return arg
|
|
|
|
def normalize_args(args, kwargs):
|
|
args = tuple(normalize_arg(arg) for arg in args)
|
|
kwargs = {kw: normalize_arg(arg) for kw, arg in kwargs.items()}
|
|
|
|
return args, kwargs
|
|
|
|
# There are two kinds of functions we care about - ones that return
|
|
# dates and ones that return lists of dates.
|
|
if not returns_list:
|
|
def inner_func(*args, **kwargs):
|
|
args, kwargs = normalize_args(args, kwargs)
|
|
dt = f(*args, **kwargs)
|
|
return self._attach_tzinfo(dt, self._tzinfo)
|
|
else:
|
|
def inner_func(*args, **kwargs):
|
|
args, kwargs = normalize_args(args, kwargs)
|
|
dts = f(*args, **kwargs)
|
|
return [self._attach_tzinfo(dt, self._tzinfo) for dt in dts]
|
|
|
|
return functools.wraps(f)(inner_func)
|
|
|
|
def __getattr__(self, name):
|
|
if name in self.__dict__:
|
|
return self.__dict__[name]
|
|
|
|
f = getattr(self._rrule, name)
|
|
|
|
if name in {'after', 'before'}:
|
|
return self._aware_return_wrapper(f)
|
|
elif name in {'xafter', 'xbefore', 'between'}:
|
|
return self._aware_return_wrapper(f, returns_list=True)
|
|
else:
|
|
return f
|
|
|
|
def __setstate__(self, state):
|
|
self.__dict__.update(state)
|
|
|
|
|
|
class DateLocator(ticker.Locator):
|
|
"""
|
|
Determines the tick locations when plotting dates.
|
|
|
|
This class is subclassed by other Locators and
|
|
is not meant to be used on its own.
|
|
"""
|
|
hms0d = {'byhour': 0, 'byminute': 0, 'bysecond': 0}
|
|
|
|
def __init__(self, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
self.tz = _get_tzinfo(tz)
|
|
|
|
def set_tzinfo(self, tz):
|
|
"""
|
|
Set timezone info.
|
|
|
|
Parameters
|
|
----------
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
self.tz = _get_tzinfo(tz)
|
|
|
|
def datalim_to_dt(self):
|
|
"""Convert axis data interval to datetime objects."""
|
|
dmin, dmax = self.axis.get_data_interval()
|
|
if dmin > dmax:
|
|
dmin, dmax = dmax, dmin
|
|
|
|
return num2date(dmin, self.tz), num2date(dmax, self.tz)
|
|
|
|
def viewlim_to_dt(self):
|
|
"""Convert the view interval to datetime objects."""
|
|
vmin, vmax = self.axis.get_view_interval()
|
|
if vmin > vmax:
|
|
vmin, vmax = vmax, vmin
|
|
return num2date(vmin, self.tz), num2date(vmax, self.tz)
|
|
|
|
def _get_unit(self):
|
|
"""
|
|
Return how many days a unit of the locator is; used for
|
|
intelligent autoscaling.
|
|
"""
|
|
return 1
|
|
|
|
def _get_interval(self):
|
|
"""
|
|
Return the number of units for each tick.
|
|
"""
|
|
return 1
|
|
|
|
def nonsingular(self, vmin, vmax):
|
|
"""
|
|
Given the proposed upper and lower extent, adjust the range
|
|
if it is too close to being singular (i.e. a range of ~0).
|
|
"""
|
|
if not np.isfinite(vmin) or not np.isfinite(vmax):
|
|
# Except if there is no data, then use 1970 as default.
|
|
return (date2num(datetime.date(1970, 1, 1)),
|
|
date2num(datetime.date(1970, 1, 2)))
|
|
if vmax < vmin:
|
|
vmin, vmax = vmax, vmin
|
|
unit = self._get_unit()
|
|
interval = self._get_interval()
|
|
if abs(vmax - vmin) < 1e-6:
|
|
vmin -= 2 * unit * interval
|
|
vmax += 2 * unit * interval
|
|
return vmin, vmax
|
|
|
|
|
|
class RRuleLocator(DateLocator):
|
|
# use the dateutil rrule instance
|
|
|
|
def __init__(self, o, tz=None):
|
|
super().__init__(tz)
|
|
self.rule = o
|
|
|
|
def __call__(self):
|
|
# if no data have been set, this will tank with a ValueError
|
|
try:
|
|
dmin, dmax = self.viewlim_to_dt()
|
|
except ValueError:
|
|
return []
|
|
|
|
return self.tick_values(dmin, dmax)
|
|
|
|
def tick_values(self, vmin, vmax):
|
|
start, stop = self._create_rrule(vmin, vmax)
|
|
dates = self.rule.between(start, stop, True)
|
|
if len(dates) == 0:
|
|
return date2num([vmin, vmax])
|
|
return self.raise_if_exceeds(date2num(dates))
|
|
|
|
def _create_rrule(self, vmin, vmax):
|
|
# set appropriate rrule dtstart and until and return
|
|
# start and end
|
|
delta = relativedelta(vmax, vmin)
|
|
|
|
# We need to cap at the endpoints of valid datetime
|
|
try:
|
|
start = vmin - delta
|
|
except (ValueError, OverflowError):
|
|
# cap
|
|
start = datetime.datetime(1, 1, 1, 0, 0, 0,
|
|
tzinfo=datetime.timezone.utc)
|
|
|
|
try:
|
|
stop = vmax + delta
|
|
except (ValueError, OverflowError):
|
|
# cap
|
|
stop = datetime.datetime(9999, 12, 31, 23, 59, 59,
|
|
tzinfo=datetime.timezone.utc)
|
|
|
|
self.rule.set(dtstart=start, until=stop)
|
|
|
|
return vmin, vmax
|
|
|
|
def _get_unit(self):
|
|
# docstring inherited
|
|
freq = self.rule._rrule._freq
|
|
return self.get_unit_generic(freq)
|
|
|
|
@staticmethod
|
|
def get_unit_generic(freq):
|
|
if freq == YEARLY:
|
|
return DAYS_PER_YEAR
|
|
elif freq == MONTHLY:
|
|
return DAYS_PER_MONTH
|
|
elif freq == WEEKLY:
|
|
return DAYS_PER_WEEK
|
|
elif freq == DAILY:
|
|
return 1.0
|
|
elif freq == HOURLY:
|
|
return 1.0 / HOURS_PER_DAY
|
|
elif freq == MINUTELY:
|
|
return 1.0 / MINUTES_PER_DAY
|
|
elif freq == SECONDLY:
|
|
return 1.0 / SEC_PER_DAY
|
|
else:
|
|
# error
|
|
return -1 # or should this just return '1'?
|
|
|
|
def _get_interval(self):
|
|
return self.rule._rrule._interval
|
|
|
|
|
|
class AutoDateLocator(DateLocator):
|
|
"""
|
|
On autoscale, this class picks the best `DateLocator` to set the view
|
|
limits and the tick locations.
|
|
|
|
Attributes
|
|
----------
|
|
intervald : dict
|
|
|
|
Mapping of tick frequencies to multiples allowed for that ticking.
|
|
The default is ::
|
|
|
|
self.intervald = {
|
|
YEARLY : [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500,
|
|
1000, 2000, 4000, 5000, 10000],
|
|
MONTHLY : [1, 2, 3, 4, 6],
|
|
DAILY : [1, 2, 3, 7, 14, 21],
|
|
HOURLY : [1, 2, 3, 4, 6, 12],
|
|
MINUTELY: [1, 5, 10, 15, 30],
|
|
SECONDLY: [1, 5, 10, 15, 30],
|
|
MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500,
|
|
1000, 2000, 5000, 10000, 20000, 50000,
|
|
100000, 200000, 500000, 1000000],
|
|
}
|
|
|
|
where the keys are defined in `dateutil.rrule`.
|
|
|
|
The interval is used to specify multiples that are appropriate for
|
|
the frequency of ticking. For instance, every 7 days is sensible
|
|
for daily ticks, but for minutes/seconds, 15 or 30 make sense.
|
|
|
|
When customizing, you should only modify the values for the existing
|
|
keys. You should not add or delete entries.
|
|
|
|
Example for forcing ticks every 3 hours::
|
|
|
|
locator = AutoDateLocator()
|
|
locator.intervald[HOURLY] = [3] # only show every 3 hours
|
|
"""
|
|
|
|
def __init__(self, tz=None, minticks=5, maxticks=None,
|
|
interval_multiples=True):
|
|
"""
|
|
Parameters
|
|
----------
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
minticks : int
|
|
The minimum number of ticks desired; controls whether ticks occur
|
|
yearly, monthly, etc.
|
|
maxticks : int
|
|
The maximum number of ticks desired; controls the interval between
|
|
ticks (ticking every other, every 3, etc.). For fine-grained
|
|
control, this can be a dictionary mapping individual rrule
|
|
frequency constants (YEARLY, MONTHLY, etc.) to their own maximum
|
|
number of ticks. This can be used to keep the number of ticks
|
|
appropriate to the format chosen in `AutoDateFormatter`. Any
|
|
frequency not specified in this dictionary is given a default
|
|
value.
|
|
interval_multiples : bool, default: True
|
|
Whether ticks should be chosen to be multiple of the interval,
|
|
locking them to 'nicer' locations. For example, this will force
|
|
the ticks to be at hours 0, 6, 12, 18 when hourly ticking is done
|
|
at 6 hour intervals.
|
|
"""
|
|
super().__init__(tz=tz)
|
|
self._freq = YEARLY
|
|
self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY,
|
|
SECONDLY, MICROSECONDLY]
|
|
self.minticks = minticks
|
|
|
|
self.maxticks = {YEARLY: 11, MONTHLY: 12, DAILY: 11, HOURLY: 12,
|
|
MINUTELY: 11, SECONDLY: 11, MICROSECONDLY: 8}
|
|
if maxticks is not None:
|
|
try:
|
|
self.maxticks.update(maxticks)
|
|
except TypeError:
|
|
# Assume we were given an integer. Use this as the maximum
|
|
# number of ticks for every frequency and create a
|
|
# dictionary for this
|
|
self.maxticks = dict.fromkeys(self._freqs, maxticks)
|
|
self.interval_multiples = interval_multiples
|
|
self.intervald = {
|
|
YEARLY: [1, 2, 4, 5, 10, 20, 40, 50, 100, 200, 400, 500,
|
|
1000, 2000, 4000, 5000, 10000],
|
|
MONTHLY: [1, 2, 3, 4, 6],
|
|
DAILY: [1, 2, 3, 7, 14, 21],
|
|
HOURLY: [1, 2, 3, 4, 6, 12],
|
|
MINUTELY: [1, 5, 10, 15, 30],
|
|
SECONDLY: [1, 5, 10, 15, 30],
|
|
MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000,
|
|
5000, 10000, 20000, 50000, 100000, 200000, 500000,
|
|
1000000],
|
|
}
|
|
if interval_multiples:
|
|
# Swap "3" for "4" in the DAILY list; If we use 3 we get bad
|
|
# tick loc for months w/ 31 days: 1, 4, ..., 28, 31, 1
|
|
# If we use 4 then we get: 1, 5, ... 25, 29, 1
|
|
self.intervald[DAILY] = [1, 2, 4, 7, 14]
|
|
|
|
self._byranges = [None, range(1, 13), range(1, 32),
|
|
range(0, 24), range(0, 60), range(0, 60), None]
|
|
|
|
def __call__(self):
|
|
# docstring inherited
|
|
dmin, dmax = self.viewlim_to_dt()
|
|
locator = self.get_locator(dmin, dmax)
|
|
return locator()
|
|
|
|
def tick_values(self, vmin, vmax):
|
|
return self.get_locator(vmin, vmax).tick_values(vmin, vmax)
|
|
|
|
def nonsingular(self, vmin, vmax):
|
|
# whatever is thrown at us, we can scale the unit.
|
|
# But default nonsingular date plots at an ~4 year period.
|
|
if not np.isfinite(vmin) or not np.isfinite(vmax):
|
|
# Except if there is no data, then use 1970 as default.
|
|
return (date2num(datetime.date(1970, 1, 1)),
|
|
date2num(datetime.date(1970, 1, 2)))
|
|
if vmax < vmin:
|
|
vmin, vmax = vmax, vmin
|
|
if vmin == vmax:
|
|
vmin = vmin - DAYS_PER_YEAR * 2
|
|
vmax = vmax + DAYS_PER_YEAR * 2
|
|
return vmin, vmax
|
|
|
|
def _get_unit(self):
|
|
if self._freq in [MICROSECONDLY]:
|
|
return 1. / MUSECONDS_PER_DAY
|
|
else:
|
|
return RRuleLocator.get_unit_generic(self._freq)
|
|
|
|
def get_locator(self, dmin, dmax):
|
|
"""Pick the best locator based on a distance."""
|
|
delta = relativedelta(dmax, dmin)
|
|
tdelta = dmax - dmin
|
|
|
|
# take absolute difference
|
|
if dmin > dmax:
|
|
delta = -delta
|
|
tdelta = -tdelta
|
|
# The following uses a mix of calls to relativedelta and timedelta
|
|
# methods because there is incomplete overlap in the functionality of
|
|
# these similar functions, and it's best to avoid doing our own math
|
|
# whenever possible.
|
|
numYears = float(delta.years)
|
|
numMonths = numYears * MONTHS_PER_YEAR + delta.months
|
|
numDays = tdelta.days # Avoids estimates of days/month, days/year.
|
|
numHours = numDays * HOURS_PER_DAY + delta.hours
|
|
numMinutes = numHours * MIN_PER_HOUR + delta.minutes
|
|
numSeconds = np.floor(tdelta.total_seconds())
|
|
numMicroseconds = np.floor(tdelta.total_seconds() * 1e6)
|
|
|
|
nums = [numYears, numMonths, numDays, numHours, numMinutes,
|
|
numSeconds, numMicroseconds]
|
|
|
|
use_rrule_locator = [True] * 6 + [False]
|
|
|
|
# Default setting of bymonth, etc. to pass to rrule
|
|
# [unused (for year), bymonth, bymonthday, byhour, byminute,
|
|
# bysecond, unused (for microseconds)]
|
|
byranges = [None, 1, 1, 0, 0, 0, None]
|
|
|
|
# Loop over all the frequencies and try to find one that gives at
|
|
# least a minticks tick positions. Once this is found, look for
|
|
# an interval from a list specific to that frequency that gives no
|
|
# more than maxticks tick positions. Also, set up some ranges
|
|
# (bymonth, etc.) as appropriate to be passed to rrulewrapper.
|
|
for i, (freq, num) in enumerate(zip(self._freqs, nums)):
|
|
# If this particular frequency doesn't give enough ticks, continue
|
|
if num < self.minticks:
|
|
# Since we're not using this particular frequency, set
|
|
# the corresponding by_ to None so the rrule can act as
|
|
# appropriate
|
|
byranges[i] = None
|
|
continue
|
|
|
|
# Find the first available interval that doesn't give too many
|
|
# ticks
|
|
for interval in self.intervald[freq]:
|
|
if num <= interval * (self.maxticks[freq] - 1):
|
|
break
|
|
else:
|
|
if not (self.interval_multiples and freq == DAILY):
|
|
_api.warn_external(
|
|
f"AutoDateLocator was unable to pick an appropriate "
|
|
f"interval for this date range. It may be necessary "
|
|
f"to add an interval value to the AutoDateLocator's "
|
|
f"intervald dictionary. Defaulting to {interval}.")
|
|
|
|
# Set some parameters as appropriate
|
|
self._freq = freq
|
|
|
|
if self._byranges[i] and self.interval_multiples:
|
|
byranges[i] = self._byranges[i][::interval]
|
|
if i in (DAILY, WEEKLY):
|
|
if interval == 14:
|
|
# just make first and 15th. Avoids 30th.
|
|
byranges[i] = [1, 15]
|
|
elif interval == 7:
|
|
byranges[i] = [1, 8, 15, 22]
|
|
|
|
interval = 1
|
|
else:
|
|
byranges[i] = self._byranges[i]
|
|
break
|
|
else:
|
|
interval = 1
|
|
|
|
if (freq == YEARLY) and self.interval_multiples:
|
|
locator = YearLocator(interval, tz=self.tz)
|
|
elif use_rrule_locator[i]:
|
|
_, bymonth, bymonthday, byhour, byminute, bysecond, _ = byranges
|
|
rrule = rrulewrapper(self._freq, interval=interval,
|
|
dtstart=dmin, until=dmax,
|
|
bymonth=bymonth, bymonthday=bymonthday,
|
|
byhour=byhour, byminute=byminute,
|
|
bysecond=bysecond)
|
|
|
|
locator = RRuleLocator(rrule, tz=self.tz)
|
|
else:
|
|
locator = MicrosecondLocator(interval, tz=self.tz)
|
|
if date2num(dmin) > 70 * 365 and interval < 1000:
|
|
_api.warn_external(
|
|
'Plotting microsecond time intervals for dates far from '
|
|
f'the epoch (time origin: {get_epoch()}) is not well-'
|
|
'supported. See matplotlib.dates.set_epoch to change the '
|
|
'epoch.')
|
|
|
|
locator.set_axis(self.axis)
|
|
return locator
|
|
|
|
|
|
class YearLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on a given day of each year that is a multiple of base.
|
|
|
|
Examples::
|
|
|
|
# Tick every year on Jan 1st
|
|
locator = YearLocator()
|
|
|
|
# Tick every 5 years on July 4th
|
|
locator = YearLocator(5, month=7, day=4)
|
|
"""
|
|
def __init__(self, base=1, month=1, day=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
base : int, default: 1
|
|
Mark ticks every *base* years.
|
|
month : int, default: 1
|
|
The month on which to place the ticks, starting from 1. Default is
|
|
January.
|
|
day : int, default: 1
|
|
The day on which to place the ticks.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
rule = rrulewrapper(YEARLY, interval=base, bymonth=month,
|
|
bymonthday=day, **self.hms0d)
|
|
super().__init__(rule, tz=tz)
|
|
self.base = ticker._Edge_integer(base, 0)
|
|
|
|
def _create_rrule(self, vmin, vmax):
|
|
# 'start' needs to be a multiple of the interval to create ticks on
|
|
# interval multiples when the tick frequency is YEARLY
|
|
ymin = max(self.base.le(vmin.year) * self.base.step, 1)
|
|
ymax = min(self.base.ge(vmax.year) * self.base.step, 9999)
|
|
|
|
c = self.rule._construct
|
|
replace = {'year': ymin,
|
|
'month': c.get('bymonth', 1),
|
|
'day': c.get('bymonthday', 1),
|
|
'hour': 0, 'minute': 0, 'second': 0}
|
|
|
|
start = vmin.replace(**replace)
|
|
stop = start.replace(year=ymax)
|
|
self.rule.set(dtstart=start, until=stop)
|
|
|
|
return start, stop
|
|
|
|
|
|
class MonthLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on occurrences of each month, e.g., 1, 3, 12.
|
|
"""
|
|
def __init__(self, bymonth=None, bymonthday=1, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
bymonth : int or list of int, default: all months
|
|
Ticks will be placed on every month in *bymonth*. Default is
|
|
``range(1, 13)``, i.e. every month.
|
|
bymonthday : int, default: 1
|
|
The day on which to place the ticks.
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
if bymonth is None:
|
|
bymonth = range(1, 13)
|
|
|
|
rule = rrulewrapper(MONTHLY, bymonth=bymonth, bymonthday=bymonthday,
|
|
interval=interval, **self.hms0d)
|
|
super().__init__(rule, tz=tz)
|
|
|
|
|
|
class WeekdayLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on occurrences of each weekday.
|
|
"""
|
|
|
|
def __init__(self, byweekday=1, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
byweekday : int or list of int, default: all days
|
|
Ticks will be placed on every weekday in *byweekday*. Default is
|
|
every day.
|
|
|
|
Elements of *byweekday* must be one of MO, TU, WE, TH, FR, SA,
|
|
SU, the constants from :mod:`dateutil.rrule`, which have been
|
|
imported into the :mod:`matplotlib.dates` namespace.
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
rule = rrulewrapper(DAILY, byweekday=byweekday,
|
|
interval=interval, **self.hms0d)
|
|
super().__init__(rule, tz=tz)
|
|
|
|
|
|
class DayLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on occurrences of each day of the month. For example,
|
|
1, 15, 30.
|
|
"""
|
|
def __init__(self, bymonthday=None, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
bymonthday : int or list of int, default: all days
|
|
Ticks will be placed on every day in *bymonthday*. Default is
|
|
``bymonthday=range(1, 32)``, i.e., every day of the month.
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
if interval != int(interval) or interval < 1:
|
|
raise ValueError("interval must be an integer greater than 0")
|
|
if bymonthday is None:
|
|
bymonthday = range(1, 32)
|
|
|
|
rule = rrulewrapper(DAILY, bymonthday=bymonthday,
|
|
interval=interval, **self.hms0d)
|
|
super().__init__(rule, tz=tz)
|
|
|
|
|
|
class HourLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on occurrences of each hour.
|
|
"""
|
|
def __init__(self, byhour=None, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
byhour : int or list of int, default: all hours
|
|
Ticks will be placed on every hour in *byhour*. Default is
|
|
``byhour=range(24)``, i.e., every hour.
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
if byhour is None:
|
|
byhour = range(24)
|
|
|
|
rule = rrulewrapper(HOURLY, byhour=byhour, interval=interval,
|
|
byminute=0, bysecond=0)
|
|
super().__init__(rule, tz=tz)
|
|
|
|
|
|
class MinuteLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on occurrences of each minute.
|
|
"""
|
|
def __init__(self, byminute=None, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
byminute : int or list of int, default: all minutes
|
|
Ticks will be placed on every minute in *byminute*. Default is
|
|
``byminute=range(60)``, i.e., every minute.
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
if byminute is None:
|
|
byminute = range(60)
|
|
|
|
rule = rrulewrapper(MINUTELY, byminute=byminute, interval=interval,
|
|
bysecond=0)
|
|
super().__init__(rule, tz=tz)
|
|
|
|
|
|
class SecondLocator(RRuleLocator):
|
|
"""
|
|
Make ticks on occurrences of each second.
|
|
"""
|
|
def __init__(self, bysecond=None, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
bysecond : int or list of int, default: all seconds
|
|
Ticks will be placed on every second in *bysecond*. Default is
|
|
``bysecond = range(60)``, i.e., every second.
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
if bysecond is None:
|
|
bysecond = range(60)
|
|
|
|
rule = rrulewrapper(SECONDLY, bysecond=bysecond, interval=interval)
|
|
super().__init__(rule, tz=tz)
|
|
|
|
|
|
class MicrosecondLocator(DateLocator):
|
|
"""
|
|
Make ticks on regular intervals of one or more microsecond(s).
|
|
|
|
.. note::
|
|
|
|
By default, Matplotlib uses a floating point representation of time in
|
|
days since the epoch, so plotting data with
|
|
microsecond time resolution does not work well for
|
|
dates that are far (about 70 years) from the epoch (check with
|
|
`~.dates.get_epoch`).
|
|
|
|
If you want sub-microsecond resolution time plots, it is strongly
|
|
recommended to use floating point seconds, not datetime-like
|
|
time representation.
|
|
|
|
If you really must use datetime.datetime() or similar and still
|
|
need microsecond precision, change the time origin via
|
|
`.dates.set_epoch` to something closer to the dates being plotted.
|
|
See :doc:`/gallery/ticks/date_precision_and_epochs`.
|
|
|
|
"""
|
|
def __init__(self, interval=1, tz=None):
|
|
"""
|
|
Parameters
|
|
----------
|
|
interval : int, default: 1
|
|
The interval between each iteration. For example, if
|
|
``interval=2``, mark every second occurrence.
|
|
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
|
|
Ticks timezone. If a string, *tz* is passed to `dateutil.tz`.
|
|
"""
|
|
super().__init__(tz=tz)
|
|
self._interval = interval
|
|
self._wrapped_locator = ticker.MultipleLocator(interval)
|
|
|
|
def set_axis(self, axis):
|
|
self._wrapped_locator.set_axis(axis)
|
|
return super().set_axis(axis)
|
|
|
|
def __call__(self):
|
|
# if no data have been set, this will tank with a ValueError
|
|
try:
|
|
dmin, dmax = self.viewlim_to_dt()
|
|
except ValueError:
|
|
return []
|
|
|
|
return self.tick_values(dmin, dmax)
|
|
|
|
def tick_values(self, vmin, vmax):
|
|
nmin, nmax = date2num((vmin, vmax))
|
|
t0 = np.floor(nmin)
|
|
nmax = nmax - t0
|
|
nmin = nmin - t0
|
|
nmin *= MUSECONDS_PER_DAY
|
|
nmax *= MUSECONDS_PER_DAY
|
|
|
|
ticks = self._wrapped_locator.tick_values(nmin, nmax)
|
|
|
|
ticks = ticks / MUSECONDS_PER_DAY + t0
|
|
return ticks
|
|
|
|
def _get_unit(self):
|
|
# docstring inherited
|
|
return 1. / MUSECONDS_PER_DAY
|
|
|
|
def _get_interval(self):
|
|
# docstring inherited
|
|
return self._interval
|
|
|
|
|
|
class DateConverter(units.ConversionInterface):
|
|
"""
|
|
Converter for `datetime.date` and `datetime.datetime` data, or for
|
|
date/time data represented as it would be converted by `date2num`.
|
|
|
|
The 'unit' tag for such data is None or a `~datetime.tzinfo` instance.
|
|
"""
|
|
|
|
def __init__(self, *, interval_multiples=True):
|
|
self._interval_multiples = interval_multiples
|
|
super().__init__()
|
|
|
|
def axisinfo(self, unit, axis):
|
|
"""
|
|
Return the `~matplotlib.units.AxisInfo` for *unit*.
|
|
|
|
*unit* is a `~datetime.tzinfo` instance or None.
|
|
The *axis* argument is required but not used.
|
|
"""
|
|
tz = unit
|
|
|
|
majloc = AutoDateLocator(tz=tz,
|
|
interval_multiples=self._interval_multiples)
|
|
majfmt = AutoDateFormatter(majloc, tz=tz)
|
|
datemin = datetime.date(1970, 1, 1)
|
|
datemax = datetime.date(1970, 1, 2)
|
|
|
|
return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='',
|
|
default_limits=(datemin, datemax))
|
|
|
|
@staticmethod
|
|
def convert(value, unit, axis):
|
|
"""
|
|
If *value* is not already a number or sequence of numbers, convert it
|
|
with `date2num`.
|
|
|
|
The *unit* and *axis* arguments are not used.
|
|
"""
|
|
return date2num(value)
|
|
|
|
@staticmethod
|
|
def default_units(x, axis):
|
|
"""
|
|
Return the `~datetime.tzinfo` instance of *x* or of its first element,
|
|
or None
|
|
"""
|
|
if isinstance(x, np.ndarray):
|
|
x = x.ravel()
|
|
|
|
try:
|
|
x = cbook._safe_first_finite(x)
|
|
except (TypeError, StopIteration):
|
|
pass
|
|
|
|
try:
|
|
return x.tzinfo
|
|
except AttributeError:
|
|
pass
|
|
return None
|
|
|
|
|
|
class ConciseDateConverter(DateConverter):
|
|
# docstring inherited
|
|
|
|
def __init__(self, formats=None, zero_formats=None, offset_formats=None,
|
|
show_offset=True, *, interval_multiples=True):
|
|
self._formats = formats
|
|
self._zero_formats = zero_formats
|
|
self._offset_formats = offset_formats
|
|
self._show_offset = show_offset
|
|
self._interval_multiples = interval_multiples
|
|
super().__init__()
|
|
|
|
def axisinfo(self, unit, axis):
|
|
# docstring inherited
|
|
tz = unit
|
|
majloc = AutoDateLocator(tz=tz,
|
|
interval_multiples=self._interval_multiples)
|
|
majfmt = ConciseDateFormatter(majloc, tz=tz, formats=self._formats,
|
|
zero_formats=self._zero_formats,
|
|
offset_formats=self._offset_formats,
|
|
show_offset=self._show_offset)
|
|
datemin = datetime.date(1970, 1, 1)
|
|
datemax = datetime.date(1970, 1, 2)
|
|
return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='',
|
|
default_limits=(datemin, datemax))
|
|
|
|
|
|
class _SwitchableDateConverter:
|
|
"""
|
|
Helper converter-like object that generates and dispatches to
|
|
temporary ConciseDateConverter or DateConverter instances based on
|
|
:rc:`date.converter` and :rc:`date.interval_multiples`.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _get_converter():
|
|
converter_cls = {
|
|
"concise": ConciseDateConverter, "auto": DateConverter}[
|
|
mpl.rcParams["date.converter"]]
|
|
interval_multiples = mpl.rcParams["date.interval_multiples"]
|
|
return converter_cls(interval_multiples=interval_multiples)
|
|
|
|
def axisinfo(self, *args, **kwargs):
|
|
return self._get_converter().axisinfo(*args, **kwargs)
|
|
|
|
def default_units(self, *args, **kwargs):
|
|
return self._get_converter().default_units(*args, **kwargs)
|
|
|
|
def convert(self, *args, **kwargs):
|
|
return self._get_converter().convert(*args, **kwargs)
|
|
|
|
|
|
units.registry[np.datetime64] = \
|
|
units.registry[datetime.date] = \
|
|
units.registry[datetime.datetime] = \
|
|
_SwitchableDateConverter()
|