1958 lines
67 KiB
Python
1958 lines
67 KiB
Python
|
"""
|
||
|
Matplotlib provides sophisticated date plotting capabilities, standing on the
|
||
|
shoulders of python :mod:`datetime` and the add-on module :mod:`dateutil`.
|
||
|
|
||
|
.. _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_and_spines/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, tickers and formatters are timezone aware.
|
||
|
If no explicit timezone is provided, :rc:`timezone` is assumed. If you want to
|
||
|
use a custom time zone, pass a `datetime.tzinfo` instance with the tz keyword
|
||
|
argument to `num2date`, `~.Axes.plot_date`, and any custom date tickers or
|
||
|
locators you create.
|
||
|
|
||
|
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 tickers
|
||
|
------------
|
||
|
|
||
|
Most of the date tickers can locate single or multiple values. 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 tickers 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 `matplotlib.dates.rrulewrapper`.
|
||
|
`.rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` which
|
||
|
allow almost arbitrary date tick specifications. See :doc:`rrule example
|
||
|
</gallery/ticks_and_spines/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. E.g. 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
|
||
|
---------------
|
||
|
|
||
|
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.
|
||
|
|
||
|
* `IndexDateFormatter`: date plots with implicit *x* indexing.
|
||
|
"""
|
||
|
|
||
|
import datetime
|
||
|
import functools
|
||
|
import logging
|
||
|
import math
|
||
|
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',
|
||
|
'epoch2num', 'num2epoch', 'set_epoch', 'get_epoch', 'DateFormatter',
|
||
|
'ConciseDateFormatter', 'IndexDateFormatter', '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')
|
||
|
|
||
|
|
||
|
_log = logging.getLogger(__name__)
|
||
|
UTC = datetime.timezone.utc
|
||
|
|
||
|
|
||
|
def _get_rc_timezone():
|
||
|
"""Retrieve the preferred timezone from the rcParams dictionary."""
|
||
|
s = mpl.rcParams['timezone']
|
||
|
if s == 'UTC':
|
||
|
return UTC
|
||
|
return dateutil.tz.gettz(s)
|
||
|
|
||
|
|
||
|
"""
|
||
|
Time-related constants.
|
||
|
"""
|
||
|
EPOCH_OFFSET = float(datetime.datetime(1970, 1, 1).toordinal())
|
||
|
# EPOCH_OFFSET is not used by matplotlib
|
||
|
JULIAN_OFFSET = 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
|
||
|
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_and_spines/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
|
||
|
|
||
|
if _epoch is None:
|
||
|
_epoch = mpl.rcParams['date.epoch']
|
||
|
return _epoch
|
||
|
|
||
|
|
||
|
def _dt64_to_ordinalf(d):
|
||
|
"""
|
||
|
Convert `numpy.datetime64` or an 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)
|
||
|
try:
|
||
|
dt[d_int == NaT_int] = np.nan
|
||
|
except TypeError:
|
||
|
if d_int == NaT_int:
|
||
|
dt = 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`.
|
||
|
"""
|
||
|
|
||
|
if tz is None:
|
||
|
tz = _get_rc_timezone()
|
||
|
|
||
|
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 = [dateutil.parser.parse(s, default=default) for s in 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.
|
||
|
"""
|
||
|
if hasattr(d, "values"):
|
||
|
# this unpacks pandas series or dataframes...
|
||
|
d = d.values
|
||
|
|
||
|
# make an iterable, but save state to unpack later:
|
||
|
iterable = np.iterable(d)
|
||
|
if not iterable:
|
||
|
d = [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 = _dt64_to_ordinalf(d)
|
||
|
|
||
|
return d if iterable else d[0]
|
||
|
|
||
|
|
||
|
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 = JULIAN_OFFSET - ep0 + ep
|
||
|
return np.subtract(j, dt) # Handles both scalar & nonscalar j.
|
||
|
|
||
|
|
||
|
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 = 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, default: :rc:`timezone`
|
||
|
Timezone of *x*.
|
||
|
|
||
|
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 addition of one here is a historical artifact. Also, note that the
|
||
|
Gregorian calendar is assumed; this is not universal practice.
|
||
|
For details, see the module docstring.
|
||
|
"""
|
||
|
if tz is None:
|
||
|
tz = _get_rc_timezone()
|
||
|
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 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):
|
||
|
# Braces ensure dashes are not spaced like binary operators.
|
||
|
return '$\\mathdefault{' + text.replace('-', '{-}') + '}$'
|
||
|
|
||
|
|
||
|
## date tickers and formatters ###
|
||
|
|
||
|
|
||
|
class DateFormatter(ticker.Formatter):
|
||
|
"""
|
||
|
Format a tick (in days since the epoch) with a
|
||
|
`~datetime.datetime.strftime` format string.
|
||
|
"""
|
||
|
|
||
|
@_api.deprecated("3.3")
|
||
|
@property
|
||
|
def illegal_s(self):
|
||
|
return re.compile(r"((^|[^%])(%%)*%s)")
|
||
|
|
||
|
def __init__(self, fmt, tz=None, *, usetex=None):
|
||
|
"""
|
||
|
Parameters
|
||
|
----------
|
||
|
fmt : str
|
||
|
`~datetime.datetime.strftime` format string
|
||
|
tz : `datetime.tzinfo`, default: :rc:`timezone`
|
||
|
Ticks timezone.
|
||
|
usetex : bool, default: :rc:`text.usetex`
|
||
|
To enable/disable the use of TeX's math mode for rendering the
|
||
|
results of the formatter.
|
||
|
"""
|
||
|
if tz is None:
|
||
|
tz = _get_rc_timezone()
|
||
|
self.fmt = fmt
|
||
|
self.tz = tz
|
||
|
self._usetex = (usetex if usetex is not None else
|
||
|
mpl.rcParams['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 = tz
|
||
|
|
||
|
|
||
|
@_api.deprecated("3.3")
|
||
|
class IndexDateFormatter(ticker.Formatter):
|
||
|
"""Use with `.IndexLocator` to cycle format strings by index."""
|
||
|
|
||
|
def __init__(self, t, fmt, tz=None):
|
||
|
"""
|
||
|
Parameters
|
||
|
----------
|
||
|
t : list of float
|
||
|
A sequence of dates (floating point days).
|
||
|
fmt : str
|
||
|
A `~datetime.datetime.strftime` format string.
|
||
|
"""
|
||
|
if tz is None:
|
||
|
tz = _get_rc_timezone()
|
||
|
self.t = t
|
||
|
self.fmt = fmt
|
||
|
self.tz = tz
|
||
|
|
||
|
def __call__(self, x, pos=0):
|
||
|
"""Return the label for time *x* at position *pos*."""
|
||
|
ind = int(round(x))
|
||
|
if ind >= len(self.t) or ind <= 0:
|
||
|
return ''
|
||
|
return num2date(self.t[ind], self.tz).strftime(self.fmt)
|
||
|
|
||
|
|
||
|
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, optional
|
||
|
Passed to `.dates.date2num`.
|
||
|
|
||
|
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_and_spines/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('offsetfmts 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 = (usetex if usetex is not None else
|
||
|
mpl.rcParams['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
|
||
|
|
||
|
# 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):
|
||
|
if len(np.unique(tickdate[:, level])) > 1:
|
||
|
# level is less than 2 so a year is already present in the axis
|
||
|
if (level < 2):
|
||
|
self.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 self.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)
|
||
|
|
||
|
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`.
|
||
|
|
||
|
The AutoDateFormatter has a scale dictionary that maps the scale
|
||
|
of the tick (the distance in days between one major tick) and a
|
||
|
format string. The default looks like this::
|
||
|
|
||
|
self.scaled = {
|
||
|
DAYS_PER_YEAR: rcParams['date.autoformat.year'],
|
||
|
DAYS_PER_MONTH: rcParams['date.autoformat.month'],
|
||
|
1.0: rcParams['date.autoformat.day'],
|
||
|
1. / HOURS_PER_DAY: rcParams['date.autoformat.hour'],
|
||
|
1. / (MINUTES_PER_DAY): rcParams['date.autoformat.minute'],
|
||
|
1. / (SEC_PER_DAY): rcParams['date.autoformat.second'],
|
||
|
1. / (MUSECONDS_PER_DAY): rcParams['date.autoformat.microsecond'],
|
||
|
}
|
||
|
|
||
|
The algorithm picks the key in the dictionary that is >= the
|
||
|
current scale and uses that format string. You can customize this
|
||
|
dictionary by doing::
|
||
|
|
||
|
>>> locator = AutoDateLocator()
|
||
|
>>> formatter = AutoDateFormatter(locator)
|
||
|
>>> formatter.scaled[1/(24.*60.)] = '%M:%S' # only show min and sec
|
||
|
|
||
|
A custom `.FuncFormatter` can also be used. 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
|
||
|
>>> from matplotlib.ticker import FuncFormatter
|
||
|
>>> formatter.scaled[1/(24.*60.)] = FuncFormatter(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, optional
|
||
|
Passed to `.dates.date2num`.
|
||
|
|
||
|
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 = (usetex if usetex is not None else
|
||
|
mpl.rcParams['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('Unexpected type passed to {0!r}.'.format(self))
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
class rrulewrapper:
|
||
|
def __init__(self, freq, tzinfo=None, **kwargs):
|
||
|
kwargs['freq'] = freq
|
||
|
self._base_tzinfo = tzinfo
|
||
|
|
||
|
self._update_rrule(**kwargs)
|
||
|
|
||
|
def set(self, **kwargs):
|
||
|
self._construct.update(kwargs)
|
||
|
|
||
|
self._update_rrule(**self._construct)
|
||
|
|
||
|
def _update_rrule(self, **kwargs):
|
||
|
tzinfo = self._base_tzinfo
|
||
|
|
||
|
# rrule does not play nicely with time zones - 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 : `datetime.tzinfo`
|
||
|
"""
|
||
|
if tz is None:
|
||
|
tz = _get_rc_timezone()
|
||
|
self.tz = tz
|
||
|
|
||
|
def set_tzinfo(self, tz):
|
||
|
"""
|
||
|
Set time zone info.
|
||
|
"""
|
||
|
self.tz = 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 2000-2010 as default.
|
||
|
return (date2num(datetime.date(2000, 1, 1)),
|
||
|
date2num(datetime.date(2010, 1, 1)))
|
||
|
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):
|
||
|
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)
|
||
|
|
||
|
dates = self.rule.between(vmin, vmax, True)
|
||
|
if len(dates) == 0:
|
||
|
return date2num([vmin, vmax])
|
||
|
return self.raise_if_exceeds(date2num(dates))
|
||
|
|
||
|
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 : `datetime.tzinfo`
|
||
|
Ticks timezone.
|
||
|
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)
|
||
|
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 2000-2010 as default.
|
||
|
return (date2num(datetime.date(2000, 1, 1)),
|
||
|
date2num(datetime.date(2010, 1, 1)))
|
||
|
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 an 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, 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)
|
||
|
|
||
|
if self.axis is not None:
|
||
|
locator.set_view_interval(*self.axis.get_view_interval())
|
||
|
locator.set_data_interval(*self.axis.get_data_interval())
|
||
|
return locator
|
||
|
|
||
|
|
||
|
class YearLocator(DateLocator):
|
||
|
"""
|
||
|
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):
|
||
|
"""
|
||
|
Mark years that are multiple of base on a given month and day
|
||
|
(default jan 1).
|
||
|
"""
|
||
|
super().__init__(tz)
|
||
|
self.base = ticker._Edge_integer(base, 0)
|
||
|
self.replaced = {'month': month,
|
||
|
'day': day,
|
||
|
'hour': 0,
|
||
|
'minute': 0,
|
||
|
'second': 0,
|
||
|
}
|
||
|
if not hasattr(tz, 'localize'):
|
||
|
# if tz is pytz, we need to do this w/ the localize fcn,
|
||
|
# otherwise datetime.replace works fine...
|
||
|
self.replaced['tzinfo'] = tz
|
||
|
|
||
|
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):
|
||
|
ymin = self.base.le(vmin.year) * self.base.step
|
||
|
ymax = self.base.ge(vmax.year) * self.base.step
|
||
|
|
||
|
vmin = vmin.replace(year=ymin, **self.replaced)
|
||
|
if hasattr(self.tz, 'localize'):
|
||
|
# look after pytz
|
||
|
if not vmin.tzinfo:
|
||
|
vmin = self.tz.localize(vmin, is_dst=True)
|
||
|
|
||
|
ticks = [vmin]
|
||
|
|
||
|
while True:
|
||
|
dt = ticks[-1]
|
||
|
if dt.year >= ymax:
|
||
|
return date2num(ticks)
|
||
|
year = dt.year + self.base.step
|
||
|
dt = dt.replace(year=year, **self.replaced)
|
||
|
if hasattr(self.tz, 'localize'):
|
||
|
# look after pytz
|
||
|
if not dt.tzinfo:
|
||
|
dt = self.tz.localize(dt, is_dst=True)
|
||
|
|
||
|
ticks.append(dt)
|
||
|
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
Mark every month in *bymonth*; *bymonth* can be an int or
|
||
|
sequence. Default is ``range(1, 13)``, i.e. every month.
|
||
|
|
||
|
*interval* is the interval between each iteration. For
|
||
|
example, if ``interval=2``, mark every second occurrence.
|
||
|
"""
|
||
|
if bymonth is None:
|
||
|
bymonth = range(1, 13)
|
||
|
elif isinstance(bymonth, np.ndarray):
|
||
|
# This fixes a bug in dateutil <= 2.3 which prevents the use of
|
||
|
# numpy arrays in (among other things) the bymonthday, byweekday
|
||
|
# and bymonth parameters.
|
||
|
bymonth = [x.item() for x in bymonth.astype(int)]
|
||
|
|
||
|
rule = rrulewrapper(MONTHLY, bymonth=bymonth, bymonthday=bymonthday,
|
||
|
interval=interval, **self.hms0d)
|
||
|
super().__init__(rule, tz)
|
||
|
|
||
|
|
||
|
class WeekdayLocator(RRuleLocator):
|
||
|
"""
|
||
|
Make ticks on occurrences of each weekday.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, byweekday=1, interval=1, tz=None):
|
||
|
"""
|
||
|
Mark every weekday in *byweekday*; *byweekday* can be a number or
|
||
|
sequence.
|
||
|
|
||
|
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* specifies the number of weeks to skip. For example,
|
||
|
``interval=2`` plots every second week.
|
||
|
"""
|
||
|
if isinstance(byweekday, np.ndarray):
|
||
|
# This fixes a bug in dateutil <= 2.3 which prevents the use of
|
||
|
# numpy arrays in (among other things) the bymonthday, byweekday
|
||
|
# and bymonth parameters.
|
||
|
[x.item() for x in byweekday.astype(int)]
|
||
|
|
||
|
rule = rrulewrapper(DAILY, byweekday=byweekday,
|
||
|
interval=interval, **self.hms0d)
|
||
|
super().__init__(rule, 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):
|
||
|
"""
|
||
|
Mark every day in *bymonthday*; *bymonthday* can be an int or sequence.
|
||
|
|
||
|
Default is to tick every day of the month: ``bymonthday=range(1, 32)``.
|
||
|
"""
|
||
|
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)
|
||
|
elif isinstance(bymonthday, np.ndarray):
|
||
|
# This fixes a bug in dateutil <= 2.3 which prevents the use of
|
||
|
# numpy arrays in (among other things) the bymonthday, byweekday
|
||
|
# and bymonth parameters.
|
||
|
bymonthday = [x.item() for x in bymonthday.astype(int)]
|
||
|
|
||
|
rule = rrulewrapper(DAILY, bymonthday=bymonthday,
|
||
|
interval=interval, **self.hms0d)
|
||
|
super().__init__(rule, tz)
|
||
|
|
||
|
|
||
|
class HourLocator(RRuleLocator):
|
||
|
"""
|
||
|
Make ticks on occurrences of each hour.
|
||
|
"""
|
||
|
def __init__(self, byhour=None, interval=1, tz=None):
|
||
|
"""
|
||
|
Mark every hour in *byhour*; *byhour* can be an int or sequence.
|
||
|
Default is to tick every hour: ``byhour=range(24)``
|
||
|
|
||
|
*interval* is the interval between each iteration. For
|
||
|
example, if ``interval=2``, mark every second occurrence.
|
||
|
"""
|
||
|
if byhour is None:
|
||
|
byhour = range(24)
|
||
|
|
||
|
rule = rrulewrapper(HOURLY, byhour=byhour, interval=interval,
|
||
|
byminute=0, bysecond=0)
|
||
|
super().__init__(rule, tz)
|
||
|
|
||
|
|
||
|
class MinuteLocator(RRuleLocator):
|
||
|
"""
|
||
|
Make ticks on occurrences of each minute.
|
||
|
"""
|
||
|
def __init__(self, byminute=None, interval=1, tz=None):
|
||
|
"""
|
||
|
Mark every minute in *byminute*; *byminute* can be an int or
|
||
|
sequence. Default is to tick every minute: ``byminute=range(60)``
|
||
|
|
||
|
*interval* is the interval between each iteration. For
|
||
|
example, if ``interval=2``, mark every second occurrence.
|
||
|
"""
|
||
|
if byminute is None:
|
||
|
byminute = range(60)
|
||
|
|
||
|
rule = rrulewrapper(MINUTELY, byminute=byminute, interval=interval,
|
||
|
bysecond=0)
|
||
|
super().__init__(rule, tz)
|
||
|
|
||
|
|
||
|
class SecondLocator(RRuleLocator):
|
||
|
"""
|
||
|
Make ticks on occurrences of each second.
|
||
|
"""
|
||
|
def __init__(self, bysecond=None, interval=1, tz=None):
|
||
|
"""
|
||
|
Mark every second in *bysecond*; *bysecond* can be an int or
|
||
|
sequence. Default is to tick every second: ``bysecond = range(60)``
|
||
|
|
||
|
*interval* is the interval between each iteration. For
|
||
|
example, if ``interval=2``, mark every second occurrence.
|
||
|
|
||
|
"""
|
||
|
if bysecond is None:
|
||
|
bysecond = range(60)
|
||
|
|
||
|
rule = rrulewrapper(SECONDLY, bysecond=bysecond, interval=interval)
|
||
|
super().__init__(rule, 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_and_spines/date_precision_and_epochs`.
|
||
|
|
||
|
"""
|
||
|
def __init__(self, interval=1, tz=None):
|
||
|
"""
|
||
|
*interval* is the interval between each iteration. For
|
||
|
example, if ``interval=2``, mark every second microsecond.
|
||
|
|
||
|
"""
|
||
|
self._interval = interval
|
||
|
self._wrapped_locator = ticker.MultipleLocator(interval)
|
||
|
self.tz = tz
|
||
|
|
||
|
def set_axis(self, axis):
|
||
|
self._wrapped_locator.set_axis(axis)
|
||
|
return super().set_axis(axis)
|
||
|
|
||
|
def set_view_interval(self, vmin, vmax):
|
||
|
self._wrapped_locator.set_view_interval(vmin, vmax)
|
||
|
return super().set_view_interval(vmin, vmax)
|
||
|
|
||
|
def set_data_interval(self, vmin, vmax):
|
||
|
self._wrapped_locator.set_data_interval(vmin, vmax)
|
||
|
return super().set_data_interval(vmin, vmax)
|
||
|
|
||
|
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
|
||
|
|
||
|
|
||
|
def epoch2num(e):
|
||
|
"""
|
||
|
Convert UNIX time to days since Matplotlib epoch.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
e : list of floats
|
||
|
Time in seconds since 1970-01-01.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
`numpy.array`
|
||
|
Time in days since Matplotlib epoch (see `~.dates.get_epoch()`).
|
||
|
"""
|
||
|
|
||
|
dt = (np.datetime64('1970-01-01T00:00:00', 's') -
|
||
|
np.datetime64(get_epoch(), 's')).astype(float)
|
||
|
|
||
|
return (dt + np.asarray(e)) / SEC_PER_DAY
|
||
|
|
||
|
|
||
|
def num2epoch(d):
|
||
|
"""
|
||
|
Convert days since Matplotlib epoch to UNIX time.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
d : list of floats
|
||
|
Time in days since Matplotlib epoch (see `~.dates.get_epoch()`).
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
`numpy.array`
|
||
|
Time in seconds since 1970-01-01.
|
||
|
"""
|
||
|
dt = (np.datetime64('1970-01-01T00:00:00', 's') -
|
||
|
np.datetime64(get_epoch(), 's')).astype(float)
|
||
|
|
||
|
return np.asarray(d) * SEC_PER_DAY - dt
|
||
|
|
||
|
|
||
|
def date_ticker_factory(span, tz=None, numticks=5):
|
||
|
"""
|
||
|
Create a date locator with *numticks* (approx) and a date formatter
|
||
|
for *span* in days. Return value is (locator, formatter).
|
||
|
"""
|
||
|
|
||
|
if span == 0:
|
||
|
span = 1 / HOURS_PER_DAY
|
||
|
|
||
|
mins = span * MINUTES_PER_DAY
|
||
|
hrs = span * HOURS_PER_DAY
|
||
|
days = span
|
||
|
wks = span / DAYS_PER_WEEK
|
||
|
months = span / DAYS_PER_MONTH # Approx
|
||
|
years = span / DAYS_PER_YEAR # Approx
|
||
|
|
||
|
if years > numticks:
|
||
|
locator = YearLocator(int(years / numticks), tz=tz) # define
|
||
|
fmt = '%Y'
|
||
|
elif months > numticks:
|
||
|
locator = MonthLocator(tz=tz)
|
||
|
fmt = '%b %Y'
|
||
|
elif wks > numticks:
|
||
|
locator = WeekdayLocator(tz=tz)
|
||
|
fmt = '%a, %b %d'
|
||
|
elif days > numticks:
|
||
|
locator = DayLocator(interval=math.ceil(days / numticks), tz=tz)
|
||
|
fmt = '%b %d'
|
||
|
elif hrs > numticks:
|
||
|
locator = HourLocator(interval=math.ceil(hrs / numticks), tz=tz)
|
||
|
fmt = '%H:%M\n%b %d'
|
||
|
elif mins > numticks:
|
||
|
locator = MinuteLocator(interval=math.ceil(mins / numticks), tz=tz)
|
||
|
fmt = '%H:%M:%S'
|
||
|
else:
|
||
|
locator = MinuteLocator(tz=tz)
|
||
|
fmt = '%H:%M:%S'
|
||
|
|
||
|
formatter = DateFormatter(fmt, tz=tz)
|
||
|
return locator, formatter
|
||
|
|
||
|
|
||
|
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 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 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(2000, 1, 1)
|
||
|
datemax = datetime.date(2010, 1, 1)
|
||
|
|
||
|
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 tzinfo instance of *x* or of its first element, or None
|
||
|
"""
|
||
|
if isinstance(x, np.ndarray):
|
||
|
x = x.ravel()
|
||
|
|
||
|
try:
|
||
|
x = cbook.safe_first_element(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(2000, 1, 1)
|
||
|
datemax = datetime.date(2010, 1, 1)
|
||
|
return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='',
|
||
|
default_limits=(datemin, datemax))
|
||
|
|
||
|
|
||
|
class _rcParam_helper:
|
||
|
"""
|
||
|
This helper class is so that we can set the converter for dates
|
||
|
via the validator for the rcParams `date.converter` and
|
||
|
`date.interval_multiples`. Never instatiated.
|
||
|
"""
|
||
|
|
||
|
conv_st = 'auto'
|
||
|
int_mult = True
|
||
|
|
||
|
@classmethod
|
||
|
def set_converter(cls, s):
|
||
|
"""Called by validator for rcParams date.converter"""
|
||
|
if s not in ['concise', 'auto']:
|
||
|
raise ValueError('Converter must be one of "concise" or "auto"')
|
||
|
cls.conv_st = s
|
||
|
cls.register_converters()
|
||
|
|
||
|
@classmethod
|
||
|
def set_int_mult(cls, b):
|
||
|
"""Called by validator for rcParams date.interval_multiples"""
|
||
|
cls.int_mult = b
|
||
|
cls.register_converters()
|
||
|
|
||
|
@classmethod
|
||
|
def register_converters(cls):
|
||
|
"""
|
||
|
Helper to register the date converters when rcParams `date.converter`
|
||
|
and `date.interval_multiples` are changed. Called by the helpers
|
||
|
above.
|
||
|
"""
|
||
|
if cls.conv_st == 'concise':
|
||
|
converter = ConciseDateConverter
|
||
|
else:
|
||
|
converter = DateConverter
|
||
|
|
||
|
interval_multiples = cls.int_mult
|
||
|
convert = converter(interval_multiples=interval_multiples)
|
||
|
units.registry[np.datetime64] = convert
|
||
|
units.registry[datetime.date] = convert
|
||
|
units.registry[datetime.datetime] = convert
|