|
| 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