Skip to content

Commit e8c7579

Browse files
authored
Merge pull request #15008 from jklymak/enh-add-variable-epoch
ENH: add variable epoch
2 parents ad320c2 + a3fbc49 commit e8c7579

File tree

11 files changed

+438
-153
lines changed

11 files changed

+438
-153
lines changed

doc/api/api_changes_3.3/deprecations.rst

+5
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,8 @@ experimental and may change in the future.
491491
``testing.compare.make_external_conversion_command``
492492
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
493493
... is deprecated.
494+
495+
`.epoch2num` and `.num2epoch` are deprecated
496+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
497+
These are unused and can be easily reproduced by other date tools.
498+
`.get_epoch` will return Matplotlib's epoch.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Dates now use a modern epoch
2+
----------------------------
3+
4+
Matplotlib converts dates to days since an epoch using `.dates.date2num` (via
5+
`matplotlib.units`). Previously, an epoch of ``0000-12-31T00:00:00`` was used
6+
so that ``0001-01-01`` was converted to 1.0. An epoch so distant in the
7+
past meant that a modern date was not able to preserve microseconds because
8+
2000 years times the 2^(-52) resolution of a 64-bit float gives 14
9+
microseconds.
10+
11+
Here we change the default epoch to the more reasonable UNIX default of
12+
``1970-01-01T00:00:00`` which for a modern date has 0.35 microsecond
13+
resolution. (Finer resolution is not possible because we rely on
14+
`datetime.datetime` for the date locators). Access to the epoch is provided
15+
by `~.dates.get_epoch`, and there is a new :rc:`date.epoch` rcParam. The user
16+
may also call `~.dates.set_epoch`, but it must be set *before* any date
17+
conversion or plotting is used.
18+
19+
If you have data stored as ordinal floats in the old epoch, a simple
20+
conversion (using the new epoch) is::
21+
22+
new_ordinal = old_ordinal + mdates.date2num(np.datetime64('0000-12-31'))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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.
9+
10+
Before Matplotlib 3.3, the default for this conversion returns a float that was
11+
days since "0000-12-31T00:00:00". As of Matplotlib 3.3, the default is
12+
days from "1970-01-01T00:00:00". This allows more resolution for modern
13+
dates. "2020-01-01" with the old epoch converted to 730120, and a 64-bit
14+
floating point number has a resolution of 2^{-52}, or approximately
15+
14 microseconds, so microsecond precision was lost. With the new default
16+
epoch "2020-01-01" is 10957.0, so the achievable resolution is 0.21
17+
microseconds.
18+
19+
"""
20+
import datetime
21+
import numpy as np
22+
23+
import matplotlib
24+
import matplotlib.pyplot as plt
25+
import matplotlib.dates as mdates
26+
27+
28+
def _reset_epoch_for_tutorial():
29+
"""
30+
Users (and downstream libraries) should not use the private method of
31+
resetting the epoch.
32+
"""
33+
mdates._reset_epoch_test_example()
34+
35+
36+
#############################################################################
37+
# Datetime
38+
# --------
39+
#
40+
# Python `.datetime` objects have microsecond resolution, so with the
41+
# old default matplotlib dates could not round-trip full-resolution datetime
42+
# objects.
43+
44+
old_epoch = '0000-12-31T00:00:00'
45+
new_epoch = '1970-01-01T00:00:00'
46+
47+
_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
48+
mdates.set_epoch(old_epoch) # old epoch (pre MPL 3.3)
49+
50+
date1 = datetime.datetime(2000, 1, 1, 0, 10, 0, 12,
51+
tzinfo=datetime.timezone.utc)
52+
mdate1 = mdates.date2num(date1)
53+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
54+
date2 = mdates.num2date(mdate1)
55+
print('After Roundtrip: ', date2)
56+
57+
#############################################################################
58+
# Note this is only a round-off error, and there is no problem for
59+
# dates closer to the old epoch:
60+
61+
date1 = datetime.datetime(10, 1, 1, 0, 10, 0, 12,
62+
tzinfo=datetime.timezone.utc)
63+
mdate1 = mdates.date2num(date1)
64+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
65+
date2 = mdates.num2date(mdate1)
66+
print('After Roundtrip: ', date2)
67+
68+
#############################################################################
69+
# If a user wants to use modern dates at microsecond precision, they
70+
# can change the epoch using `~.set_epoch`. However, the epoch has to be
71+
# set before any date operations to prevent confusion between different
72+
# epochs. Trying to change the epoch later will raise a `RuntimeError`.
73+
74+
try:
75+
mdates.set_epoch(new_epoch) # this is the new MPL 3.3 default.
76+
except RuntimeError as e:
77+
print('RuntimeError:', str(e))
78+
79+
#############################################################################
80+
# For this tutorial, we reset the sentinel using a private method, but users
81+
# should just set the epoch once, if at all.
82+
83+
_reset_epoch_for_tutorial() # Just being done for this tutorial.
84+
mdates.set_epoch(new_epoch)
85+
86+
date1 = datetime.datetime(2020, 1, 1, 0, 10, 0, 12,
87+
tzinfo=datetime.timezone.utc)
88+
mdate1 = mdates.date2num(date1)
89+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
90+
date2 = mdates.num2date(mdate1)
91+
print('After Roundtrip: ', date2)
92+
93+
#############################################################################
94+
# datetime64
95+
# ----------
96+
#
97+
# `numpy.datetime64` objects have microsecond precision for a much larger
98+
# timespace than `.datetime` objects. However, currently Matplotlib time is
99+
# only converted back to datetime objects, which have microsecond resolution,
100+
# and years that only span 0000 to 9999.
101+
102+
_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
103+
mdates.set_epoch(new_epoch)
104+
105+
date1 = np.datetime64('2000-01-01T00:10:00.000012')
106+
mdate1 = mdates.date2num(date1)
107+
print('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)
108+
date2 = mdates.num2date(mdate1)
109+
print('After Roundtrip: ', date2)
110+
111+
#############################################################################
112+
# Plotting
113+
# --------
114+
#
115+
# This all of course has an effect on plotting. With the old default epoch
116+
# the times were rounded, leading to jumps in the data:
117+
118+
_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
119+
mdates.set_epoch(old_epoch)
120+
121+
x = np.arange('2000-01-01T00:00:00.0', '2000-01-01T00:00:00.000100',
122+
dtype='datetime64[us]')
123+
y = np.arange(0, len(x))
124+
fig, ax = plt.subplots(constrained_layout=True)
125+
ax.plot(x, y)
126+
ax.set_title('Epoch: ' + mdates.get_epoch())
127+
plt.setp(ax.xaxis.get_majorticklabels(), rotation=40)
128+
plt.show()
129+
130+
#############################################################################
131+
# For a more recent epoch, the plot is smooth:
132+
133+
_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
134+
mdates.set_epoch(new_epoch)
135+
136+
fig, ax = plt.subplots(constrained_layout=True)
137+
ax.plot(x, y)
138+
ax.set_title('Epoch: ' + mdates.get_epoch())
139+
plt.setp(ax.xaxis.get_majorticklabels(), rotation=40)
140+
plt.show()
141+
142+
_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.
143+
144+
#############################################################################
145+
# ------------
146+
#
147+
# References
148+
# """"""""""
149+
#
150+
# The use of the following functions, methods and classes is shown
151+
# in this example:
152+
153+
matplotlib.dates.num2date
154+
matplotlib.dates.date2num
155+
matplotlib.dates.set_epoch

lib/matplotlib/axes/_axes.py

-1
Original file line numberDiff line numberDiff line change
@@ -1809,7 +1809,6 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False,
18091809
self.xaxis_date(tz)
18101810
if ydate:
18111811
self.yaxis_date(tz)
1812-
18131812
ret = self.plot(x, y, fmt, **kwargs)
18141813

18151814
self._request_autoscale_view()

0 commit comments

Comments
 (0)