{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n# Date precision and epochs\n\nMatplotlib can handle `.datetime` objects and `numpy.datetime64` objects using\na unit converter that recognizes these dates and converts them to floating\npoint numbers.\n\nBefore Matplotlib 3.3, the default for this conversion returns a float that was\ndays since \"0000-12-31T00:00:00\". As of Matplotlib 3.3, the default is\ndays from \"1970-01-01T00:00:00\". This allows more resolution for modern\ndates. \"2020-01-01\" with the old epoch converted to 730120, and a 64-bit\nfloating point number has a resolution of 2^{-52}, or approximately\n14 microseconds, so microsecond precision was lost. With the new default\nepoch \"2020-01-01\" is 10957.0, so the achievable resolution is 0.21\nmicroseconds.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import datetime\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nimport matplotlib.dates as mdates\n\n\ndef _reset_epoch_for_tutorial():\n \"\"\"\n Users (and downstream libraries) should not use the private method of\n resetting the epoch.\n \"\"\"\n mdates._reset_epoch_test_example()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Datetime\n\nPython `.datetime` objects have microsecond resolution, so with the\nold default matplotlib dates could not round-trip full-resolution datetime\nobjects.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"old_epoch = '0000-12-31T00:00:00'\nnew_epoch = '1970-01-01T00:00:00'\n\n_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.\nmdates.set_epoch(old_epoch) # old epoch (pre MPL 3.3)\n\ndate1 = datetime.datetime(2000, 1, 1, 0, 10, 0, 12,\n tzinfo=datetime.timezone.utc)\nmdate1 = mdates.date2num(date1)\nprint('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)\ndate2 = mdates.num2date(mdate1)\nprint('After Roundtrip: ', date2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note this is only a round-off error, and there is no problem for\ndates closer to the old epoch:\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"date1 = datetime.datetime(10, 1, 1, 0, 10, 0, 12,\n tzinfo=datetime.timezone.utc)\nmdate1 = mdates.date2num(date1)\nprint('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)\ndate2 = mdates.num2date(mdate1)\nprint('After Roundtrip: ', date2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If a user wants to use modern dates at microsecond precision, they\ncan change the epoch using `.set_epoch`. However, the epoch has to be\nset before any date operations to prevent confusion between different\nepochs. Trying to change the epoch later will raise a `RuntimeError`.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"try:\n mdates.set_epoch(new_epoch) # this is the new MPL 3.3 default.\nexcept RuntimeError as e:\n print('RuntimeError:', str(e))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For this tutorial, we reset the sentinel using a private method, but users\nshould just set the epoch once, if at all.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"_reset_epoch_for_tutorial() # Just being done for this tutorial.\nmdates.set_epoch(new_epoch)\n\ndate1 = datetime.datetime(2020, 1, 1, 0, 10, 0, 12,\n tzinfo=datetime.timezone.utc)\nmdate1 = mdates.date2num(date1)\nprint('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)\ndate2 = mdates.num2date(mdate1)\nprint('After Roundtrip: ', date2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## datetime64\n\n`numpy.datetime64` objects have microsecond precision for a much larger\ntimespace than `.datetime` objects. However, currently Matplotlib time is\nonly converted back to datetime objects, which have microsecond resolution,\nand years that only span 0000 to 9999.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.\nmdates.set_epoch(new_epoch)\n\ndate1 = np.datetime64('2000-01-01T00:10:00.000012')\nmdate1 = mdates.date2num(date1)\nprint('Before Roundtrip: ', date1, 'Matplotlib date:', mdate1)\ndate2 = mdates.num2date(mdate1)\nprint('After Roundtrip: ', date2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plotting\n\nThis all of course has an effect on plotting. With the old default epoch\nthe times were rounded during the internal ``date2num`` conversion, leading\nto jumps in the data:\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.\nmdates.set_epoch(old_epoch)\n\nx = np.arange('2000-01-01T00:00:00.0', '2000-01-01T00:00:00.000100',\n dtype='datetime64[us]')\n# simulate the plot being made using the old epoch\nxold = np.array([mdates.num2date(mdates.date2num(d)) for d in x])\ny = np.arange(0, len(x))\n\n# resetting the Epoch so plots are comparable\n_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial.\nmdates.set_epoch(new_epoch)\n\nfig, ax = plt.subplots(layout='constrained')\nax.plot(xold, y)\nax.set_title('Epoch: ' + mdates.get_epoch())\nax.xaxis.set_tick_params(rotation=40)\nplt.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For dates plotted using the more recent epoch, the plot is smooth:\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"fig, ax = plt.subplots(layout='constrained')\nax.plot(x, y)\nax.set_title('Epoch: ' + mdates.get_epoch())\nax.xaxis.set_tick_params(rotation=40)\nplt.show()\n\n_reset_epoch_for_tutorial() # Don't do this. Just for this tutorial."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
".. admonition:: References\n\n The use of the following functions, methods, classes and modules is shown\n in this example:\n\n - `matplotlib.dates.num2date`\n - `matplotlib.dates.date2num`\n - `matplotlib.dates.set_epoch`\n\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.2"
}
},
"nbformat": 4,
"nbformat_minor": 0
}