2068 lines
70 KiB
Python
2068 lines
70 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 0001-01-01 UTC, plus 1. For example, 0001-01-01, 06:00 is 1.25,
|
||
|
not 0.25. Values < 1, i.e. dates before 0001-01-01 UTC, are not supported.
|
||
|
|
||
|
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
|
||
|
epoch2num
|
||
|
num2epoch
|
||
|
mx2num
|
||
|
drange
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
Like Python's datetime, mpl 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 mpl 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 `strftime` format strings.
|
||
|
|
||
|
* `IndexDateFormatter`: date plots with implicit *x* indexing.
|
||
|
"""
|
||
|
|
||
|
import datetime
|
||
|
import functools
|
||
|
import logging
|
||
|
import math
|
||
|
import re
|
||
|
import time
|
||
|
import warnings
|
||
|
|
||
|
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
|
||
|
from matplotlib import rcParams
|
||
|
import matplotlib.units as units
|
||
|
import matplotlib.cbook as cbook
|
||
|
import matplotlib.ticker as ticker
|
||
|
|
||
|
__all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange',
|
||
|
'epoch2num', 'num2epoch', 'mx2num', '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',
|
||
|
'seconds', 'minutes', 'hours', 'weeks')
|
||
|
|
||
|
|
||
|
_log = logging.getLogger(__name__)
|
||
|
UTC = datetime.timezone.utc
|
||
|
|
||
|
|
||
|
def _get_rc_timezone():
|
||
|
"""Retrieve the preferred timezone from the rcParams dictionary."""
|
||
|
s = matplotlib.rcParams['timezone']
|
||
|
if s == 'UTC':
|
||
|
return UTC
|
||
|
return dateutil.tz.gettz(s)
|
||
|
|
||
|
|
||
|
"""
|
||
|
Time-related constants.
|
||
|
"""
|
||
|
EPOCH_OFFSET = float(datetime.datetime(1970, 1, 1).toordinal())
|
||
|
JULIAN_OFFSET = 1721424.5 # Julian date at 0001-01-01
|
||
|
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)
|
||
|
|
||
|
|
||
|
def _to_ordinalf(dt):
|
||
|
"""
|
||
|
Convert :mod:`datetime` or :mod:`date` to the Gregorian date as UTC float
|
||
|
days, preserving hours, minutes, seconds and microseconds. Return value
|
||
|
is a :func:`float`.
|
||
|
"""
|
||
|
# Convert to UTC
|
||
|
tzi = getattr(dt, 'tzinfo', None)
|
||
|
if tzi is not None:
|
||
|
dt = dt.astimezone(UTC)
|
||
|
tzi = UTC
|
||
|
|
||
|
base = float(dt.toordinal())
|
||
|
|
||
|
# If it's sufficiently datetime-like, it will have a `date()` method
|
||
|
cdate = getattr(dt, 'date', lambda: None)()
|
||
|
if cdate is not None:
|
||
|
# Get a datetime object at midnight UTC
|
||
|
midnight_time = datetime.time(0, tzinfo=tzi)
|
||
|
|
||
|
rdt = datetime.datetime.combine(cdate, midnight_time)
|
||
|
|
||
|
# Append the seconds as a fraction of a day
|
||
|
base += (dt - rdt).total_seconds() / SEC_PER_DAY
|
||
|
|
||
|
return base
|
||
|
|
||
|
|
||
|
# a version of _to_ordinalf that can operate on numpy arrays
|
||
|
_to_ordinalf_np_vectorized = np.vectorize(_to_ordinalf)
|
||
|
|
||
|
|
||
|
def _dt64_to_ordinalf(d):
|
||
|
"""
|
||
|
Convert `numpy.datetime64` or an ndarray of those types to Gregorian
|
||
|
date as UTC float. Roundoff is via float64 precision. Practically:
|
||
|
microseconds for dates between 290301 BC, 294241 AD, milliseconds for
|
||
|
larger dates (see `numpy.datetime64`). Nanoseconds aren't possible
|
||
|
because we do times compared to ``0001-01-01T00:00:00`` (plus one day).
|
||
|
"""
|
||
|
|
||
|
# the "extra" ensures that we at least allow the dynamic range out to
|
||
|
# seconds. That should get out to +/-2e11 years.
|
||
|
extra = (d - d.astype('datetime64[s]')).astype('timedelta64[ns]')
|
||
|
t0 = np.datetime64('0001-01-01T00:00:00', 's')
|
||
|
dt = (d.astype('datetime64[s]') - t0).astype(np.float64)
|
||
|
dt += extra.astype(np.float64) / 1.0e9
|
||
|
dt = dt / SEC_PER_DAY + 1.0
|
||
|
|
||
|
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()
|
||
|
|
||
|
ix, remainder = divmod(x, 1)
|
||
|
ix = int(ix)
|
||
|
if ix < 1:
|
||
|
raise ValueError('Cannot convert {} to a date. This often happens if '
|
||
|
'non-datetime values are passed to an axis that '
|
||
|
'expects datetime objects.'.format(ix))
|
||
|
dt = datetime.datetime.fromordinal(ix).replace(tzinfo=UTC)
|
||
|
|
||
|
# Since the input date `x` float is unable to preserve microsecond
|
||
|
# precision of time representation in non-antique years, the
|
||
|
# resulting datetime is rounded to the nearest multiple of
|
||
|
# `musec_prec`. A value of 20 is appropriate for current dates.
|
||
|
musec_prec = 20
|
||
|
remainder_musec = int(round(remainder * MUSECONDS_PER_DAY / musec_prec)
|
||
|
* musec_prec)
|
||
|
|
||
|
# For people trying to plot with full microsecond precision, enable
|
||
|
# an early-year workaround
|
||
|
if x < 30 * 365:
|
||
|
remainder_musec = int(round(remainder * MUSECONDS_PER_DAY))
|
||
|
|
||
|
# add hours, minutes, seconds, microseconds
|
||
|
dt += datetime.timedelta(microseconds=remainder_musec)
|
||
|
return dt.astimezone(tz)
|
||
|
|
||
|
|
||
|
# a version of _from_ordinalf that can operate on numpy arrays
|
||
|
_from_ordinalf_np_vectorized = np.vectorize(_from_ordinalf)
|
||
|
|
||
|
|
||
|
@cbook.deprecated(
|
||
|
"3.1", alternative="time.strptime or dateutil.parser.parse or datestr2num")
|
||
|
class strpdate2num(object):
|
||
|
"""
|
||
|
Use this class to parse date strings to matplotlib datenums when
|
||
|
you know the date format string of the date you are parsing.
|
||
|
"""
|
||
|
def __init__(self, fmt):
|
||
|
""" fmt: any valid strptime format is supported """
|
||
|
self.fmt = fmt
|
||
|
|
||
|
def __call__(self, s):
|
||
|
"""s : string to be converted
|
||
|
return value: a date2num float
|
||
|
"""
|
||
|
return date2num(datetime.datetime(*time.strptime(s, self.fmt)[:6]))
|
||
|
|
||
|
|
||
|
@cbook.deprecated(
|
||
|
"3.1", alternative="time.strptime or dateutil.parser.parse or datestr2num")
|
||
|
class bytespdate2num(strpdate2num):
|
||
|
"""
|
||
|
Use this class to parse date strings to matplotlib datenums when
|
||
|
you know the date format string of the date you are parsing. See
|
||
|
:doc:`/gallery/misc/load_converter.py`.
|
||
|
"""
|
||
|
def __init__(self, fmt, encoding='utf-8'):
|
||
|
"""
|
||
|
Args:
|
||
|
fmt: any valid strptime format is supported
|
||
|
encoding: encoding to use on byte input (default: 'utf-8')
|
||
|
"""
|
||
|
super().__init__(fmt)
|
||
|
self.encoding = encoding
|
||
|
|
||
|
def __call__(self, b):
|
||
|
"""
|
||
|
Args:
|
||
|
b: byte input to be converted
|
||
|
Returns:
|
||
|
A date2num float
|
||
|
"""
|
||
|
s = b.decode(self.encoding)
|
||
|
return super().__call__(s)
|
||
|
|
||
|
|
||
|
# 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 :func:`dateutil.parser.parse`.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
d : string or sequence of strings
|
||
|
The dates to convert.
|
||
|
|
||
|
default : datetime instance, 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 (fraction part represents hours, minutes, seconds, ms)
|
||
|
since 0001-01-01 00:00:00 UTC, plus one.
|
||
|
|
||
|
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 hasattr(d, "values"):
|
||
|
# this unpacks pandas series or dataframes...
|
||
|
d = d.values
|
||
|
if not np.iterable(d):
|
||
|
if (isinstance(d, np.datetime64) or
|
||
|
(isinstance(d, np.ndarray) and
|
||
|
np.issubdtype(d.dtype, np.datetime64))):
|
||
|
return _dt64_to_ordinalf(d)
|
||
|
return _to_ordinalf(d)
|
||
|
|
||
|
else:
|
||
|
d = np.asarray(d)
|
||
|
if np.issubdtype(d.dtype, np.datetime64):
|
||
|
return _dt64_to_ordinalf(d)
|
||
|
if not d.size:
|
||
|
return d
|
||
|
return _to_ordinalf_np_vectorized(d)
|
||
|
|
||
|
|
||
|
def julian2num(j):
|
||
|
"""
|
||
|
Convert a Julian date (or sequence) to a Matplotlib date (or sequence).
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
j : float or sequence of floats
|
||
|
Julian date(s)
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
float or sequence of floats
|
||
|
Matplotlib date(s)
|
||
|
"""
|
||
|
if np.iterable(j):
|
||
|
j = np.asarray(j)
|
||
|
return j - JULIAN_OFFSET
|
||
|
|
||
|
|
||
|
def num2julian(n):
|
||
|
"""
|
||
|
Convert a Matplotlib date (or sequence) to a Julian date (or sequence).
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
n : float or sequence of floats
|
||
|
Matplotlib date(s)
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
float or sequence of floats
|
||
|
Julian date(s)
|
||
|
"""
|
||
|
if np.iterable(n):
|
||
|
n = np.asarray(n)
|
||
|
return n + JULIAN_OFFSET
|
||
|
|
||
|
|
||
|
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 0001-01-01 00:00:00 UTC, plus one.
|
||
|
tz : string, optional
|
||
|
Timezone of *x* (defaults to rcparams ``timezone``).
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
`~datetime.datetime` or sequence of `~datetime.datetime`
|
||
|
Dates are returned in timezone *tz*.
|
||
|
|
||
|
If *x* is a sequence, a sequence of :class:`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()
|
||
|
if not np.iterable(x):
|
||
|
return _from_ordinalf(x, tz)
|
||
|
else:
|
||
|
x = np.asarray(x)
|
||
|
if not x.size:
|
||
|
return x
|
||
|
return _from_ordinalf_np_vectorized(x, tz).tolist()
|
||
|
|
||
|
|
||
|
def _ordinalf_to_timedelta(x):
|
||
|
return datetime.timedelta(days=x)
|
||
|
|
||
|
|
||
|
_ordinalf_to_timedelta_np_vectorized = np.vectorize(_ordinalf_to_timedelta)
|
||
|
|
||
|
|
||
|
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`]
|
||
|
|
||
|
"""
|
||
|
if not np.iterable(x):
|
||
|
return _ordinalf_to_timedelta(x)
|
||
|
else:
|
||
|
x = np.asarray(x)
|
||
|
if not x.size:
|
||
|
return x
|
||
|
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
|
||
|
-------
|
||
|
drange : `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)
|
||
|
|
||
|
## date tickers and formatters ###
|
||
|
|
||
|
|
||
|
class DateFormatter(ticker.Formatter):
|
||
|
"""
|
||
|
Format a tick (in seconds since the epoch) with a `strftime` format string.
|
||
|
"""
|
||
|
|
||
|
illegal_s = re.compile(r"((^|[^%])(%%)*%s)")
|
||
|
|
||
|
def __init__(self, fmt, tz=None):
|
||
|
"""
|
||
|
Parameters
|
||
|
----------
|
||
|
fmt : str
|
||
|
`strftime` format string
|
||
|
tz : `tzinfo`
|
||
|
"""
|
||
|
if tz is None:
|
||
|
tz = _get_rc_timezone()
|
||
|
self.fmt = fmt
|
||
|
self.tz = tz
|
||
|
|
||
|
def __call__(self, x, pos=0):
|
||
|
if x == 0:
|
||
|
raise ValueError('DateFormatter found a value of x=0, which is '
|
||
|
'an illegal date; this usually occurs because '
|
||
|
'you have not informed the axis that it is '
|
||
|
'plotting dates, e.g., with ax.xaxis_date()')
|
||
|
return num2date(x, self.tz).strftime(self.fmt)
|
||
|
|
||
|
def set_tzinfo(self, tz):
|
||
|
self.tz = tz
|
||
|
|
||
|
@cbook.deprecated("3.0")
|
||
|
def _replace_common_substr(self, s1, s2, sub1, sub2, replacement):
|
||
|
"""Helper function for replacing substrings sub1 and sub2
|
||
|
located at the same indexes in strings s1 and s2 respectively,
|
||
|
with the string replacement. It is expected that sub1 and sub2
|
||
|
have the same length. Returns the pair s1, s2 after the
|
||
|
substitutions.
|
||
|
"""
|
||
|
# Find common indexes of substrings sub1 in s1 and sub2 in s2
|
||
|
# and make substitutions inplace. Because this is inplace,
|
||
|
# it is okay if len(replacement) != len(sub1), len(sub2).
|
||
|
i = 0
|
||
|
while True:
|
||
|
j = s1.find(sub1, i)
|
||
|
if j == -1:
|
||
|
break
|
||
|
|
||
|
i = j + 1
|
||
|
if s2[j:j + len(sub2)] != sub2:
|
||
|
continue
|
||
|
|
||
|
s1 = s1[:j] + replacement + s1[j + len(sub1):]
|
||
|
s2 = s2[:j] + replacement + s2[j + len(sub2):]
|
||
|
|
||
|
return s1, s2
|
||
|
|
||
|
@cbook.deprecated("3.0")
|
||
|
def strftime_pre_1900(self, dt, fmt=None):
|
||
|
"""Call time.strftime for years before 1900 by rolling
|
||
|
forward a multiple of 28 years.
|
||
|
|
||
|
*fmt* is a :func:`strftime` format string.
|
||
|
|
||
|
Dalke: I hope I did this math right. Every 28 years the
|
||
|
calendar repeats, except through century leap years excepting
|
||
|
the 400 year leap years. But only if you're using the Gregorian
|
||
|
calendar.
|
||
|
"""
|
||
|
if fmt is None:
|
||
|
fmt = self.fmt
|
||
|
|
||
|
# Since python's time module's strftime implementation does not
|
||
|
# support %f microsecond (but the datetime module does), use a
|
||
|
# regular expression substitution to replace instances of %f.
|
||
|
# Note that this can be useful since python's floating-point
|
||
|
# precision representation for datetime causes precision to be
|
||
|
# more accurate closer to year 0 (around the year 2000, precision
|
||
|
# can be at 10s of microseconds).
|
||
|
fmt = re.sub(r'((^|[^%])(%%)*)%f',
|
||
|
r'\g<1>{0:06d}'.format(dt.microsecond), fmt)
|
||
|
|
||
|
year = dt.year
|
||
|
# For every non-leap year century, advance by
|
||
|
# 6 years to get into the 28-year repeat cycle
|
||
|
delta = 2000 - year
|
||
|
off = 6 * (delta // 100 + delta // 400)
|
||
|
year = year + off
|
||
|
|
||
|
# Move to between the years 1973 and 2000
|
||
|
year1 = year + ((2000 - year) // 28) * 28
|
||
|
year2 = year1 + 28
|
||
|
timetuple = dt.timetuple()
|
||
|
# Generate timestamp string for year and year+28
|
||
|
s1 = time.strftime(fmt, (year1,) + timetuple[1:])
|
||
|
s2 = time.strftime(fmt, (year2,) + timetuple[1:])
|
||
|
|
||
|
# Replace instances of respective years (both 2-digit and 4-digit)
|
||
|
# that are located at the same indexes of s1, s2 with dt's year.
|
||
|
# Note that C++'s strftime implementation does not use padded
|
||
|
# zeros or padded whitespace for %y or %Y for years before 100, but
|
||
|
# uses padded zeros for %x. (For example, try the runnable examples
|
||
|
# with .tm_year in the interval [-1900, -1800] on
|
||
|
# http://en.cppreference.com/w/c/chrono/strftime.) For ease of
|
||
|
# implementation, we always use padded zeros for %y, %Y, and %x.
|
||
|
s1, s2 = self._replace_common_substr(s1, s2,
|
||
|
"{0:04d}".format(year1),
|
||
|
"{0:04d}".format(year2),
|
||
|
"{0:04d}".format(dt.year))
|
||
|
s1, s2 = self._replace_common_substr(s1, s2,
|
||
|
"{0:02d}".format(year1 % 100),
|
||
|
"{0:02d}".format(year2 % 100),
|
||
|
"{0:02d}".format(dt.year % 100))
|
||
|
return cbook.unicode_safe(s1)
|
||
|
|
||
|
@cbook.deprecated("3.0")
|
||
|
def strftime(self, dt, fmt=None):
|
||
|
"""
|
||
|
Refer to documentation for :meth:`datetime.datetime.strftime`
|
||
|
|
||
|
*fmt* is a :meth:`datetime.datetime.strftime` format string.
|
||
|
|
||
|
Warning: For years before 1900, depending upon the current
|
||
|
locale it is possible that the year displayed with %x might
|
||
|
be incorrect. For years before 100, %y and %Y will yield
|
||
|
zero-padded strings.
|
||
|
"""
|
||
|
if fmt is None:
|
||
|
fmt = self.fmt
|
||
|
fmt = self.illegal_s.sub(r"\1", fmt)
|
||
|
fmt = fmt.replace("%s", "s")
|
||
|
if dt.year >= 1900:
|
||
|
# Note: in python 3.3 this is okay for years >= 1000,
|
||
|
# refer to http://bugs.python.org/issue1777412
|
||
|
return cbook.unicode_safe(dt.strftime(fmt))
|
||
|
|
||
|
return self.strftime_pre_1900(dt, fmt)
|
||
|
|
||
|
|
||
|
class IndexDateFormatter(ticker.Formatter):
|
||
|
"""
|
||
|
Use with :class:`~matplotlib.ticker.IndexLocator` to cycle format
|
||
|
strings by index.
|
||
|
"""
|
||
|
def __init__(self, t, fmt, tz=None):
|
||
|
"""
|
||
|
*t* is a sequence of dates (floating point days). *fmt* is a
|
||
|
:func:`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(np.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):
|
||
|
"""
|
||
|
This class 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 :class:`AutoDateLocator`::
|
||
|
|
||
|
|
||
|
>>> locator = AutoDateLocator()
|
||
|
>>> formatter = ConciseDateFormatter(locator)
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
|
||
|
locator : `.ticker.Locator`
|
||
|
Locator that this axis is using.
|
||
|
|
||
|
tz : string, 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 `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
|
||
|
Whether to show the offset or not. Default is ``True``.
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
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 labled "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
|
||
|
|
||
|
def __call__(self, x, pos=None):
|
||
|
formatter = DateFormatter(self.defaultfmt, self._tz)
|
||
|
return formatter(x, pos=pos)
|
||
|
|
||
|
def format_ticks(self, values):
|
||
|
tickdatetime = [num2date(value) 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:
|
||
|
break
|
||
|
|
||
|
# 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])
|
||
|
|
||
|
return labels
|
||
|
|
||
|
def get_offset(self):
|
||
|
return self.offset_string
|
||
|
|
||
|
def format_data_short(self, value):
|
||
|
return num2date(value).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
|
||
|
|
||
|
class AutoDateFormatter(ticker.Formatter):
|
||
|
"""
|
||
|
This class attempts to figure out the best format to use. This is
|
||
|
most useful when used with the :class:`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 :class:`~matplotlib.ticker.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'):
|
||
|
"""
|
||
|
Autoformat the date labels. The default format is the one to use
|
||
|
if none of the values in ``self.scaled`` are greater than the unit
|
||
|
returned by ``locator._get_unit()``.
|
||
|
"""
|
||
|
self._locator = locator
|
||
|
self._tz = tz
|
||
|
self.defaultfmt = defaultfmt
|
||
|
self._formatter = DateFormatter(self.defaultfmt, tz)
|
||
|
self.scaled = {DAYS_PER_YEAR: rcParams['date.autoformatter.year'],
|
||
|
DAYS_PER_MONTH: rcParams['date.autoformatter.month'],
|
||
|
1.0: 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)
|
||
|
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(object):
|
||
|
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):
|
||
|
"""
|
||
|
*tz* is a :class:`tzinfo` instance.
|
||
|
"""
|
||
|
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
|
||
|
if dmin < 1:
|
||
|
raise ValueError('datalim minimum {} is less than 1 and '
|
||
|
'is an invalid Matplotlib date value. This often '
|
||
|
'happens if you pass a non-datetime '
|
||
|
'value to an axis that has datetime units'
|
||
|
.format(dmin))
|
||
|
return num2date(dmin, self.tz), num2date(dmax, self.tz)
|
||
|
|
||
|
def viewlim_to_dt(self):
|
||
|
"""
|
||
|
Converts the view interval to datetime objects.
|
||
|
"""
|
||
|
vmin, vmax = self.axis.get_view_interval()
|
||
|
if vmin > vmax:
|
||
|
vmin, vmax = vmax, vmin
|
||
|
if vmin < 1:
|
||
|
raise ValueError('view limit minimum {} is less than 1 and '
|
||
|
'is an invalid Matplotlib date value. This '
|
||
|
'often happens if you pass a non-datetime '
|
||
|
'value to an axis that has datetime units'
|
||
|
.format(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).
|
||
|
|
||
|
"""
|
||
|
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):
|
||
|
DateLocator.__init__(self, 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):
|
||
|
start = _from_ordinalf(1.0)
|
||
|
|
||
|
try:
|
||
|
stop = vmax + delta
|
||
|
except (ValueError, OverflowError):
|
||
|
# The magic number!
|
||
|
stop = _from_ordinalf(3652059.9999999)
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
Return how many days a unit of the locator is; used for
|
||
|
intelligent autoscaling.
|
||
|
"""
|
||
|
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
|
||
|
|
||
|
def autoscale(self):
|
||
|
"""
|
||
|
Set the view limits to include the data range.
|
||
|
"""
|
||
|
dmin, dmax = self.datalim_to_dt()
|
||
|
delta = relativedelta(dmax, dmin)
|
||
|
|
||
|
# We need to cap at the endpoints of valid datetime
|
||
|
try:
|
||
|
start = dmin - delta
|
||
|
except ValueError:
|
||
|
start = _from_ordinalf(1.0)
|
||
|
|
||
|
try:
|
||
|
stop = dmax + delta
|
||
|
except ValueError:
|
||
|
# The magic number!
|
||
|
stop = _from_ordinalf(3652059.9999999)
|
||
|
|
||
|
self.rule.set(dtstart=start, until=stop)
|
||
|
dmin, dmax = self.datalim_to_dt()
|
||
|
|
||
|
vmin = self.rule.before(dmin, True)
|
||
|
if not vmin:
|
||
|
vmin = dmin
|
||
|
|
||
|
vmax = self.rule.after(dmax, True)
|
||
|
if not vmax:
|
||
|
vmax = dmax
|
||
|
|
||
|
vmin = date2num(vmin)
|
||
|
vmax = date2num(vmax)
|
||
|
|
||
|
return self.nonsingular(vmin, vmax)
|
||
|
|
||
|
|
||
|
class AutoDateLocator(DateLocator):
|
||
|
"""
|
||
|
On autoscale, this class picks the best
|
||
|
:class:`DateLocator` to set the view limits and the tick
|
||
|
locations.
|
||
|
"""
|
||
|
def __init__(self, tz=None, minticks=5, maxticks=None,
|
||
|
interval_multiples=True):
|
||
|
"""
|
||
|
*minticks* is the minimum number of ticks desired, which is used to
|
||
|
select the type of ticking (yearly, monthly, etc.).
|
||
|
|
||
|
*maxticks* is the maximum number of ticks desired, which controls
|
||
|
any interval between ticks (ticking every other, every 3, etc.).
|
||
|
For really 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
|
||
|
:class:`AutoDateFormatter`. Any frequency not specified in this
|
||
|
dictionary is given a default value.
|
||
|
|
||
|
*tz* is a :class:`tzinfo` instance.
|
||
|
|
||
|
*interval_multiples* is a boolean that indicates whether ticks
|
||
|
should be chosen to be multiple of the interval. This will lock
|
||
|
ticks 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.
|
||
|
|
||
|
The AutoDateLocator has an interval dictionary that maps the
|
||
|
frequency of the tick (a constant from dateutil.rrule) and a
|
||
|
multiple allowed for that ticking. The default looks like this::
|
||
|
|
||
|
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],
|
||
|
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],
|
||
|
}
|
||
|
|
||
|
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.
|
||
|
You can customize this dictionary by doing::
|
||
|
|
||
|
locator = AutoDateLocator()
|
||
|
locator.intervald[HOURLY] = [3] # only show every 3 hours
|
||
|
"""
|
||
|
DateLocator.__init__(self, tz)
|
||
|
self._locator = YearLocator(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, 21]
|
||
|
|
||
|
self._byranges = [None, range(1, 13), range(1, 32),
|
||
|
range(0, 24), range(0, 60), range(0, 60), None]
|
||
|
|
||
|
def __call__(self):
|
||
|
'Return the locations of the ticks'
|
||
|
self.refresh()
|
||
|
return self._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 vmin == vmax:
|
||
|
vmin = vmin - DAYS_PER_YEAR * 2
|
||
|
vmax = vmax + DAYS_PER_YEAR * 2
|
||
|
return vmin, vmax
|
||
|
|
||
|
def set_axis(self, axis):
|
||
|
DateLocator.set_axis(self, axis)
|
||
|
self._locator.set_axis(axis)
|
||
|
|
||
|
def refresh(self):
|
||
|
'Refresh internal information based on current limits.'
|
||
|
dmin, dmax = self.viewlim_to_dt()
|
||
|
self._locator = self.get_locator(dmin, dmax)
|
||
|
|
||
|
def _get_unit(self):
|
||
|
if self._freq in [MICROSECONDLY]:
|
||
|
return 1. / MUSECONDS_PER_DAY
|
||
|
else:
|
||
|
return RRuleLocator.get_unit_generic(self._freq)
|
||
|
|
||
|
def autoscale(self):
|
||
|
'Try to choose the view limits intelligently.'
|
||
|
dmin, dmax = self.datalim_to_dt()
|
||
|
self._locator = self.get_locator(dmin, dmax)
|
||
|
return self._locator.autoscale()
|
||
|
|
||
|
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:
|
||
|
# We went through the whole loop without breaking, default to
|
||
|
# the last interval in the list and raise a warning
|
||
|
cbook._warn_external('AutoDateLocator was unable to pick an '
|
||
|
'appropriate interval for this date '
|
||
|
'range. It may be necessary to add an '
|
||
|
'interval value to the '
|
||
|
'AutoDateLocator\'s intervald '
|
||
|
'dictionary. Defaulting to {0}.'
|
||
|
.format(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:
|
||
|
raise ValueError('No sensible date limit could be found in the '
|
||
|
'AutoDateLocator.')
|
||
|
|
||
|
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 dmin.year > 20 and interval < 1000:
|
||
|
_log.warning('Plotting microsecond time intervals is not well '
|
||
|
'supported. Please see the MicrosecondLocator '
|
||
|
'documentation for details.')
|
||
|
|
||
|
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).
|
||
|
"""
|
||
|
DateLocator.__init__(self, 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)
|
||
|
|
||
|
def autoscale(self):
|
||
|
"""
|
||
|
Set the view limits to include the data range.
|
||
|
"""
|
||
|
dmin, dmax = self.datalim_to_dt()
|
||
|
|
||
|
ymin = self.base.le(dmin.year)
|
||
|
ymax = self.base.ge(dmax.year)
|
||
|
vmin = dmin.replace(year=ymin, **self.replaced)
|
||
|
vmin = vmin.astimezone(self.tz)
|
||
|
vmax = dmax.replace(year=ymax, **self.replaced)
|
||
|
vmax = vmax.astimezone(self.tz)
|
||
|
|
||
|
vmin = date2num(vmin)
|
||
|
vmax = date2num(vmax)
|
||
|
return self.nonsingular(vmin, vmax)
|
||
|
|
||
|
|
||
|
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)
|
||
|
RRuleLocator.__init__(self, 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)
|
||
|
RRuleLocator.__init__(self, 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 not 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)
|
||
|
RRuleLocator.__init__(self, 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)
|
||
|
RRuleLocator.__init__(self, 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)
|
||
|
RRuleLocator.__init__(self, 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)
|
||
|
RRuleLocator.__init__(self, rule, tz)
|
||
|
|
||
|
|
||
|
class MicrosecondLocator(DateLocator):
|
||
|
"""
|
||
|
Make ticks on regular intervals of one or more microsecond(s).
|
||
|
|
||
|
.. note::
|
||
|
|
||
|
Due to the floating point representation of time in days since
|
||
|
0001-01-01 UTC (plus 1), plotting data with microsecond time
|
||
|
resolution does not work well with current dates.
|
||
|
|
||
|
If you want 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, your only chance is to use very
|
||
|
early years; using year 0001 is recommended.
|
||
|
|
||
|
"""
|
||
|
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 DateLocator.set_axis(self, axis)
|
||
|
|
||
|
def set_view_interval(self, vmin, vmax):
|
||
|
self._wrapped_locator.set_view_interval(vmin, vmax)
|
||
|
return DateLocator.set_view_interval(self, vmin, vmax)
|
||
|
|
||
|
def set_data_interval(self, vmin, vmax):
|
||
|
self._wrapped_locator.set_data_interval(vmin, vmax)
|
||
|
return DateLocator.set_data_interval(self, 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))
|
||
|
nmin *= MUSECONDS_PER_DAY
|
||
|
nmax *= MUSECONDS_PER_DAY
|
||
|
ticks = self._wrapped_locator.tick_values(nmin, nmax)
|
||
|
ticks = [tick / MUSECONDS_PER_DAY for tick in ticks]
|
||
|
return ticks
|
||
|
|
||
|
def _get_unit(self):
|
||
|
"""
|
||
|
Return how many days a unit of the locator is; used for
|
||
|
intelligent autoscaling.
|
||
|
"""
|
||
|
return 1. / MUSECONDS_PER_DAY
|
||
|
|
||
|
def _get_interval(self):
|
||
|
"""
|
||
|
Return the number of units for each tick.
|
||
|
"""
|
||
|
return self._interval
|
||
|
|
||
|
|
||
|
def epoch2num(e):
|
||
|
"""
|
||
|
Convert an epoch or sequence of epochs to the new date format,
|
||
|
that is days since 0001.
|
||
|
"""
|
||
|
return EPOCH_OFFSET + np.asarray(e) / SEC_PER_DAY
|
||
|
|
||
|
|
||
|
def num2epoch(d):
|
||
|
"""
|
||
|
Convert days since 0001 to epoch. *d* can be a number or sequence.
|
||
|
"""
|
||
|
return (np.asarray(d) - EPOCH_OFFSET) * SEC_PER_DAY
|
||
|
|
||
|
|
||
|
def mx2num(mxdates):
|
||
|
"""
|
||
|
Convert mx :class:`datetime` instance (or sequence of mx
|
||
|
instances) to the new date format.
|
||
|
"""
|
||
|
scalar = False
|
||
|
if not np.iterable(mxdates):
|
||
|
scalar = True
|
||
|
mxdates = [mxdates]
|
||
|
ret = epoch2num([m.ticks() for m in mxdates])
|
||
|
if scalar:
|
||
|
return ret[0]
|
||
|
else:
|
||
|
return ret
|
||
|
|
||
|
|
||
|
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
|
||
|
|
||
|
|
||
|
@cbook.deprecated("3.1")
|
||
|
def seconds(s):
|
||
|
"""
|
||
|
Return seconds as days.
|
||
|
"""
|
||
|
return s / SEC_PER_DAY
|
||
|
|
||
|
|
||
|
@cbook.deprecated("3.1")
|
||
|
def minutes(m):
|
||
|
"""
|
||
|
Return minutes as days.
|
||
|
"""
|
||
|
return m / MINUTES_PER_DAY
|
||
|
|
||
|
|
||
|
@cbook.deprecated("3.1")
|
||
|
def hours(h):
|
||
|
"""
|
||
|
Return hours as days.
|
||
|
"""
|
||
|
return h / HOURS_PER_DAY
|
||
|
|
||
|
|
||
|
@cbook.deprecated("3.1")
|
||
|
def weeks(w):
|
||
|
"""
|
||
|
Return weeks as days.
|
||
|
"""
|
||
|
return w * DAYS_PER_WEEK
|
||
|
|
||
|
|
||
|
class DateConverter(units.ConversionInterface):
|
||
|
"""
|
||
|
Converter for datetime.date and datetime.datetime data,
|
||
|
or for date/time data represented as it would be converted
|
||
|
by :func:`date2num`.
|
||
|
|
||
|
The 'unit' tag for such data is None or a tzinfo instance.
|
||
|
"""
|
||
|
|
||
|
@staticmethod
|
||
|
def axisinfo(unit, axis):
|
||
|
"""
|
||
|
Return the :class:`~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)
|
||
|
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 :func:`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):
|
||
|
"""
|
||
|
Converter for datetime.date and datetime.datetime data,
|
||
|
or for date/time data represented as it would be converted
|
||
|
by :func:`date2num`.
|
||
|
|
||
|
The 'unit' tag for such data is None or a tzinfo instance.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, formats=None, zero_formats=None, offset_formats=None,
|
||
|
show_offset=True):
|
||
|
self._formats = formats
|
||
|
self._zero_formats = zero_formats
|
||
|
self._offset_formats = offset_formats
|
||
|
self._show_offset = show_offset
|
||
|
super().__init__()
|
||
|
|
||
|
def axisinfo(self, unit, axis):
|
||
|
"""
|
||
|
Return the :class:`~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)
|
||
|
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))
|
||
|
|
||
|
|
||
|
units.registry[np.datetime64] = DateConverter()
|
||
|
units.registry[datetime.date] = DateConverter()
|
||
|
units.registry[datetime.datetime] = DateConverter()
|