Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 12f8f99

Browse files
committed
ENH
1 parent 74a031c commit 12f8f99

File tree

2 files changed

+170
-23
lines changed

2 files changed

+170
-23
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
=========================
3+
Date Precision and Epochs
4+
=========================
5+
6+
Matplotlib can handle `.datetime` objects and `numpy.datetime64` objects using
7+
a unit converter that recognizes these dates and converts them to floating
8+
point numbers. By deafult this conversion returns a float that is days
9+
since "0000-01-01T00:00:00". This has resolution implications for modern
10+
dates: "2000-01-01" in this time frame is 730120, and a 64-bit floating point
11+
number has a resolution of 2^{-52}, or approximately 14 microseconds.
12+
13+
"""
14+
import datetime
15+
import dateutil
16+
import numpy as np
17+
18+
import matplotlib
19+
import matplotlib.pyplot as plt
20+
import matplotlib.dates as mdates
21+
22+
#############################################################################
23+
# Datetime
24+
# --------
25+
#
26+
# Python `.datetime` objects have microsecond reesolution, so by default
27+
# matplotlib dates cannot round-trip full-resolution datetime objects:
28+
29+
date1 = datetime.datetime(2000, 1, 1, 0, 10, 0, 12,
30+
tzinfo=dateutil.tz.gettz('UTC'))
31+
mdate1 = mdates.date2num(date1)
32+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
33+
date2 = mdates.num2date(mdate1)
34+
print('After Roundtrip: ', date2)
35+
36+
#############################################################################
37+
# Note this is only a round-off error, and there is no problem for
38+
# dates closer to the epoch:
39+
40+
date1 = datetime.datetime(10, 1, 1, 0, 10, 0, 12,
41+
tzinfo=dateutil.tz.gettz('UTC'))
42+
mdate1 = mdates.date2num(date1)
43+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
44+
date2 = mdates.num2date(mdate1)
45+
print('After Roundtrip: ', date2)
46+
47+
#############################################################################
48+
# If a user wants to use modern dates at micro-second precision, they
49+
# can change the epoch.
50+
51+
mdates.set_epoch('1990-01-01')
52+
53+
date1 = datetime.datetime(2000, 1, 1, 0, 10, 0, 12,
54+
tzinfo=dateutil.tz.gettz('UTC'))
55+
mdate1 = mdates.date2num(date1)
56+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
57+
date2 = mdates.num2date(mdate1)
58+
print('After Roundtrip: ', date2)
59+
60+
#############################################################################
61+
# datetime64
62+
# ----------
63+
#
64+
# `numpy.datetime64` objects have micro-second precision for a much larger
65+
# timespace than `.datetime` objects. However, currently Matplotlib time is
66+
# only converted back to datetime objects, which have microsecond resolution,
67+
# and years that only span 0000 to 9999.
68+
69+
mdates.set_epoch('1990-01-01')
70+
71+
date1 = np.datetime64('2000-01-01T00:10:00.000012')
72+
mdate1 = mdates.date2num(date1)
73+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
74+
date2 = mdates.num2date(mdate1)
75+
print('After Roundtrip: ', date2)
76+
77+
#############################################################################
78+
# Plotting
79+
# --------
80+
#
81+
# This all of course has an effect on plotting. With the default epoch
82+
# the times are rounded, leading to jumps in the data:
83+
84+
mdates.set_epoch('0000-01-01')
85+
x = np.arange('2000-01-01T00:00:00.0', '2000-01-01T00:00:00.000100',
86+
dtype='datetime64[us]')
87+
y = np.arange(0, len(x))
88+
fig, ax = plt.subplots(constrained_layout=True)
89+
ax.plot(x, y)
90+
plt.setp(ax.xaxis.get_majorticklabels(), rotation=40)
91+
plt.show()
92+
93+
#############################################################################
94+
# For a more recent epoch, the plot is smooth:
95+
96+
mdates.set_epoch('1999-01-01')
97+
fig, ax = plt.subplots(constrained_layout=True)
98+
ax.plot(x, y)
99+
plt.setp(ax.xaxis.get_majorticklabels(), rotation=40)
100+
plt.show()
101+
102+
103+
#############################################################################
104+
# ------------
105+
#
106+
# References
107+
# """"""""""
108+
#
109+
# The use of the following functions, methods and classes is shown
110+
# in this example:
111+
112+
matplotlib.dates.num2date
113+
matplotlib.dates.date2num
114+
matplotlib.dates.set_epoch

lib/matplotlib/dates.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
Matplotlib date format
99
----------------------
1010
Matplotlib represents dates using floating point numbers specifying the number
11-
of days since 0001-01-01 UTC, plus 1. For example, 0001-01-01, 06:00 is 1.25,
12-
not 0.25. Values < 1, i.e. dates before 0001-01-01 UTC, are not supported.
11+
of days since a defaul epoch of 0001-01-01 UTC, plus 1. For example,
12+
0001-01-01, 06:00 is 1.25, not 0.25. (The epoch can be changed via
13+
`.dates.set_epoch` to other dates; see
14+
:doc:`/gallery/misc/date_accuracy_and_epochs` for a discussion.)
1315
1416
There are a number of helper functions to convert between :mod:`datetime`
1517
objects and Matplotlib dates:
@@ -23,9 +25,8 @@
2325
date2num
2426
num2date
2527
num2timedelta
26-
epoch2num
27-
num2epoch
2828
drange
29+
set_epoch
2930
3031
.. note::
3132
@@ -155,7 +156,7 @@
155156
import matplotlib.ticker as ticker
156157

157158
__all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange',
158-
'epoch2num', 'num2epoch', 'mx2num', 'DateFormatter',
159+
'epoch2num', 'num2epoch', 'mx2num', 'set_epoch', 'DateFormatter',
159160
'ConciseDateFormatter', 'IndexDateFormatter', 'AutoDateFormatter',
160161
'DateLocator', 'RRuleLocator', 'AutoDateLocator', 'YearLocator',
161162
'MonthLocator', 'WeekdayLocator',
@@ -206,12 +207,42 @@ def _get_rc_timezone():
206207
MO, TU, WE, TH, FR, SA, SU)
207208
WEEKDAYS = (MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY)
208209

210+
# default epoch: passed to np.datetime64...
209211
_epoch = '0001-01-01T00:00:00'
210212

213+
211214
def set_epoch(epoch):
215+
"""
216+
Set the epoch (origin for dates) for datetime calculations. The
217+
default epoch is '0000-01-01T00:00:00', but this means "modern"
218+
dates only have an accuracy of approximately 8 microseconds in the
219+
matplotlib floating point representation. Smaller floats means more
220+
precision, so an epoch within 50 days of the dates being considered
221+
can reach 1 nanosecond resolution.
222+
223+
See :doc:`/gallery/misc/date_accuracy_and_epochs` for a discussion.
224+
225+
Parameters
226+
----------
227+
epoch : str
228+
valid UTC date parsable by `.datetime64` (do not include timezone).
229+
230+
"""
212231
global _epoch
213232
_epoch = epoch
214233

234+
235+
def get_epoch():
236+
"""
237+
Get the epoch currently used by `.dates`.
238+
239+
Returns
240+
-------
241+
epoch: str
242+
"""
243+
return _epoch
244+
245+
215246
def _to_ordinalf(dt):
216247
"""
217248
Convert :mod:`datetime` or :mod:`date` to the Gregorian date as UTC float
@@ -244,7 +275,7 @@ def _dt64_to_ordinalf(d):
244275
# seconds. That should get out to +/-2e11 years.
245276
dseconds = d.astype('datetime64[s]')
246277
extra = (d - dseconds).astype('timedelta64[ns]')
247-
t0 = np.datetime64(_epoch)
278+
t0 = np.datetime64(_epoch, 's')
248279
dt = (dseconds - t0).astype(np.float64)
249280
dt += extra.astype(np.float64) / 1.0e9
250281
dt = dt / SEC_PER_DAY + 1.0
@@ -269,15 +300,19 @@ def _from_ordinalf(x, tz=None):
269300
timezone *tz*, or if *tz* is ``None``, in the timezone specified in
270301
:rc:`timezone`.
271302
"""
303+
272304
if tz is None:
273305
tz = _get_rc_timezone()
274306

275-
dt = (np.datetime64(_epoch) +
307+
dt = (np.datetime64(_epoch) +
276308
np.timedelta64(int((x - 1.0) * MUSECONDS_PER_DAY), 'us'))
309+
if dt < np.datetime64('0001-01-01') or dt > np.datetime64('10000-01-01'):
310+
raise ValueError(f'Matplotlib date {dt} cannot be converted to a '
311+
'datetime.')
312+
dt = dt.tolist()
277313
# datetime64 is always UTC:
278-
dt = dt.tolist().replace(tzinfo=dateutil.tz.gettz('UTC'))
314+
dt = dt.replace(tzinfo=dateutil.tz.gettz('UTC'))
279315
# but maybe we are working in a different timezone so move.
280-
281316
dt = dt.astimezone(tz)
282317
if x > 30 * 365:
283318
# if x is big, round off to nearest twenty microseconds.
@@ -590,11 +625,6 @@ def __init__(self, fmt, tz=None):
590625
self.tz = tz
591626

592627
def __call__(self, x, pos=0):
593-
if x == 0:
594-
raise ValueError('DateFormatter found a value of x=0, which is '
595-
'an illegal date; this usually occurs because '
596-
'you have not informed the axis that it is '
597-
'plotting dates, e.g., with ax.xaxis_date()')
598628
return num2date(x, self.tz).strftime(self.fmt)
599629

600630
def set_tzinfo(self, tz):
@@ -1431,11 +1461,12 @@ def get_locator(self, dmin, dmax):
14311461
locator = RRuleLocator(rrule, self.tz)
14321462
else:
14331463
locator = MicrosecondLocator(interval, tz=self.tz)
1434-
if dmin.year > 20 and interval < 1000:
1464+
if date2num(dmin) > 30 * 365 and interval < 1000:
14351465
cbook._warn_external(
1436-
'Plotting microsecond time intervals is not well '
1437-
'supported; please see the MicrosecondLocator '
1438-
'documentation for details.')
1466+
'Plotting microsecond time intervals for dates far from '
1467+
f'the epoch (time origin: {_epoch}) is not well-'
1468+
'supported. See matplotlib.dates.set_epoch to change the '
1469+
'epoch.')
14391470

14401471
locator.set_axis(self.axis)
14411472

@@ -1673,17 +1704,17 @@ class MicrosecondLocator(DateLocator):
16731704
16741705
.. note::
16751706
1676-
Due to the floating point representation of time in days since
1677-
0001-01-01 UTC (plus 1), plotting data with microsecond time
1678-
resolution does not work well with current dates.
1707+
By default, Matplotlib uses a floating point representation of time in
1708+
days since 0001-01-01 UTC (plus 1), so plotting data with
1709+
microsecond time resolution does not work well with modern dates.
16791710
16801711
If you want microsecond resolution time plots, it is strongly
16811712
recommended to use floating point seconds, not datetime-like
16821713
time representation.
16831714
16841715
If you really must use datetime.datetime() or similar and still
1685-
need microsecond precision, your only chance is to use very
1686-
early years; using year 0001 is recommended.
1716+
need microsecond precision, change the time origin via
1717+
`.dates.set_epoch` to something closer to the dates being plotted.
16871718
16881719
"""
16891720
def __init__(self, interval=1, tz=None):
@@ -1744,6 +1775,7 @@ def _get_interval(self):
17441775
return self._interval
17451776

17461777

1778+
@cbook.deprecated("3.2")
17471779
def epoch2num(e):
17481780
"""
17491781
Convert an epoch or sequence of epochs to the new date format,
@@ -1752,6 +1784,7 @@ def epoch2num(e):
17521784
return EPOCH_OFFSET + np.asarray(e) / SEC_PER_DAY
17531785

17541786

1787+
@cbook.deprecated("3.2")
17551788
def num2epoch(d):
17561789
"""
17571790
Convert days since 0001 to epoch. *d* can be a number or sequence.

0 commit comments

Comments
 (0)