{ "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 }