{
  "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.14.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}