| 
 | 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  | 
0 commit comments