Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e8c7579

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

File tree

Expand file treeCollapse file tree

11 files changed

+438
-153
lines changed
Filter options
Expand file treeCollapse file tree

11 files changed

+438
-153
lines changed

‎doc/api/api_changes_3.3/deprecations.rst

Copy file name to clipboardExpand all lines: doc/api/api_changes_3.3/deprecations.rst
+5Lines changed: 5 additions & 0 deletions
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.
+22Lines changed: 22 additions & 0 deletions
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'))
+155Lines changed: 155 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
-1Lines changed: 0 additions & 1 deletion
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.