{ "cells": [ { "cell_type": "markdown", "id": "24893578-8106-4ce5-874c-eea8011b1a18", "metadata": {}, "source": [ "# Plotting Time Series\n", "\n", "Lets-Plot handles all temporal data types through a unified \"datetime\" scale (excluding duration/timedelta types, which are handled by the \"time\" scale). This is in contrast to R's ggplot2, which provides separate \"date\", \"time\", and \"datetime\" scales.\n", "\n", "**Supported temporal data types:**\n", "\n", "- Python `time` objects (time of day)\n", "- Python `date` objects\n", "- Python `datetime` objects (both naive and timezone-aware)\n", "- NumPy `datetime64` objects\n", "- Pandas Series with timezone information\n", "- Polars Series with timezone information" ] }, { "cell_type": "code", "execution_count": 1, "id": "ffb7ff9c-c23a-4aa7-a222-cafe7943b269", "metadata": {}, "outputs": [], "source": [ "from datetime import datetime, date, time, timedelta, timezone\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "from lets_plot import *" ] }, { "cell_type": "code", "execution_count": 2, "id": "f81e51b9-bc23-4d18-bc96-f7029f647dbb", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " \n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "LetsPlot.setup_html()" ] }, { "cell_type": "code", "execution_count": 3, "id": "195e8880-08f0-40ee-b591-b643535ba608", "metadata": {}, "outputs": [], "source": [ "def squiggle(x):\n", " return np.sin(3 * x) / (x * (np.cos(x) + 2))" ] }, { "cell_type": "code", "execution_count": 4, "id": "d0f9940b-3602-4cfe-b352-e82c0b47a5e3", "metadata": {}, "outputs": [], "source": [ "N = 50\n", "xs = np.linspace(1, 25, N)\n", "ys = [squiggle(x) for x in xs ]" ] }, { "cell_type": "markdown", "id": "8228e825-a0b3-484b-ac8a-7f4b9006ab16", "metadata": {}, "source": [ "#### Local Time\n", "\n", "Python `time` objects represent time-of-day values (local/clock time) independent of any specific date. \n", "\n", "The datetime scale renders these with default tooltips and scale breaks optimized for hours or smaller time units." ] }, { "cell_type": "code", "execution_count": 5, "id": "dedf32b6-2cd6-4800-8cb8-73220c3aa5fe", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create time objects at regular intervals: every 12 minutes starting from 8:00 AM\n", "start_time = time(8, 0) # 8:00 AM\n", "interval_minutes = 12\n", "xs_time = []\n", "for i in range(N):\n", " total_minutes = i * interval_minutes\n", " hours = 8 + total_minutes // 60\n", " minutes = total_minutes % 60\n", " xs_time.append(time(hours, minutes))\n", "\n", "(ggplot(data = dict(xs=xs_time, ys=ys),\n", " mapping=aes('xs', 'ys'))\n", " + geom_band(\n", " xmin=time(12, 30), # <-- Use a compatible datatype ('time' here) for a constant value.\n", " xmax=time(13, 30),\n", " tooltips=layer_tooltips()\n", " .title('Lunch Time')\n", " .line('^xmin - ^xmax')\n", " .line('in any time zone'))\n", " + geom_line(\n", " tooltips=layer_tooltips()\n", " .line(\"Time:|@xs\")\n", " .format('^x', '%H:%M:%S'),\n", " color='#1380A1', size=2)\n", " + ggtb() + ggsize(800, 400)) + theme(axis_title='blank')" ] }, { "cell_type": "markdown", "id": "79a4c704-d67c-4826-b36a-2b9de8ef87d7", "metadata": {}, "source": [ "#### Date\n", "\n", "Python `date` objects represent calendar dates without time information. \n", "\n", "`Date` objects are inherently timezone-agnostic since they contain no time component - a date like \"2024-01-15\" represents the same calendar day regardless of timezone.\n", "\n", "The datetime scale renders these with default tooltips and scale breaks optimized for days or larger time units (weeks, months, years) depending on the data range." ] }, { "cell_type": "code", "execution_count": 6, "id": "157613fb-8704-4162-a292-916b6e797751", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# This covers March 15 - May 3, 2025\n", "start_date = date(2025, 3, 15)\n", "xs_date = []\n", "for i in range(N):\n", " xs_date.append(start_date + timedelta(days=i))\n", "\n", "(ggplot(data = dict(xs=xs_date, ys=ys),\n", " mapping=aes('xs', 'ys'))\n", " + geom_vline(\n", " xintercept=date(2025, 4, 1),\n", " tooltips=layer_tooltips()\n", " .title(\"April Fools' Day\")\n", " .format('^xintercept', '%a, %b %e, %Y'),\n", " linetype='dotted', size=1.5)\n", " + geom_line(\n", " tooltips=layer_tooltips()\n", " .line('Date:|@xs')\n", " .format('^x', '%b %d, %Y'),\n", " color='#1380A1', size=2)\n", " + ggtb() + ggsize(800, 400)) + theme(axis_title='blank')" ] }, { "cell_type": "markdown", "id": "ef24e242-afa7-4d06-be77-5a5296746681", "metadata": {}, "source": [ "#### Date-Time\n", "\n", "Python `datetime` objects represent both date and time information, either timezone-naive or timezone-aware. \n", "\n", "The datetime scale automatically adapts tooltips and scale breaks based on the data's temporal resolution.\n", "\n", "**Timezone handling:** If no timezone information is present (naive datetime), Lets-Plot assumes UTC timezone. For timezone-aware datetime objects, the timezone information from the data is preserved and used for rendering." ] }, { "cell_type": "code", "execution_count": 7, "id": "ffd5212f-d8bd-494a-aba7-b7e001040c9d", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Berlin DST transition: March 30, 2025 at 2:00 AM\n", "# Cover ±3 days: March 27 - April 2, 2025 (6 days total)\n", "\n", "# Generate equally spaced UNIX timestamps (in seconds)\n", "start_timestamp = datetime(2025, 3, 27, 0, 0, tzinfo=timezone.utc).timestamp()\n", "end_timestamp = datetime(2025, 4, 2, 23, 59, tzinfo=timezone.utc).timestamp()\n", "unix_timestamps = np.linspace(start_timestamp, end_timestamp, N)\n", "\n", "# Convert to pandas datetime series with Berlin timezone\n", "xs_datetime = pd.to_datetime(unix_timestamps, unit='s', utc=True).tz_convert('Europe/Berlin')\n", "\n", "df = pd.DataFrame({\n", " 'xs': xs_datetime,\n", " 'ys': ys\n", "})\n", "\n", "p = (ggplot(data = df,\n", " mapping=aes('xs', 'ys'))\n", " + geom_vline(\n", " xintercept=pd.Timestamp('2025-03-30 01:59:59', tz='Europe/Berlin'),\n", " tooltips=layer_tooltips()\n", " .title('Berlin \"spring forward\" transition')\n", " .line('^xintercept').format('^xintercept', '%b %d, %Y %H:%M:%S')\n", " .line('2:00 AM → 3:00 AM'),\n", " linetype='dotted', size=1.5)\n", " + geom_line(\n", " tooltips=layer_tooltips()\n", " .line('@xs')\n", " .format('^x', '%b %d, %Y %H:%M'),\n", " color='#1380A1', size=2)\n", " + ggtb() + ggsize(800, 400)) + theme(axis_title='blank')\n", "\n", "p" ] }, { "cell_type": "markdown", "id": "dfcf668d-fd50-4117-be67-b74fc147e68d", "metadata": {}, "source": [ "#### Zoomed-in View: Berlin Start of DST Transition Details\n", "\n", "This plot zooms into the DST transition period: ±8 hours around March 30, 2025 at 2:00 AM.\n", "\n", "Notice that the time 2025-03-30 02:00:00 through 2025-03-30 02:59:59 does not exist in Europe/Berlin timezone - clocks spring forward directly from 01:59:59 to 03:00:00, creating a gap in the timeline." ] }, { "cell_type": "code", "execution_count": 8, "id": "6331bd7d-27a3-473b-a381-b1e6f6353cfd", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "p + coord_cartesian(\n", " xlim=[\n", " pd.Timestamp('2025-03-29 21:00:00', tz='Europe/Berlin'),\n", " pd.Timestamp('2025-03-30 08:00:00', tz='Europe/Berlin')\n", " ]\n", ")" ] }, { "cell_type": "markdown", "id": "8c8b37f7-c768-438d-819f-3830e5c14058", "metadata": {}, "source": [ "#### Berlin End of DST Transition Details\n", "\n", "The mirroring phenomenon can be observed during Berlin's end of DST transition: around October 26, 2025 at 2:00 AM.\n", "\n", "Notice that the time 2025-10-26 02:00:00 through 2025-10-26 02:59:59 appears twice in Europe/Berlin timezone - clocks fall back from 02:59:59 to 02:00:00, creating a duplication in the timeline where the same hour occurs twice." ] }, { "cell_type": "code", "execution_count": 9, "id": "42a1294c-d225-4d69-929a-edd6f91cba88", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Berlin DST transition back: October 26, 2025 at 3:00 AM → 2:00 AM\n", "\n", "# Generate equally spaced UNIX timestamps (in seconds)\n", "start_timestamp = datetime(2025, 10, 23, 0, 0, tzinfo=timezone.utc).timestamp()\n", "end_timestamp = datetime(2025, 10, 29, 23, 59, tzinfo=timezone.utc).timestamp()\n", "unix_timestamps = np.linspace(start_timestamp, end_timestamp, N)\n", "\n", "# Convert to pandas datetime series with Berlin timezone\n", "xs_datetime = pd.to_datetime(unix_timestamps, unit='s', utc=True).tz_convert('Europe/Berlin')\n", "\n", "df = pd.DataFrame({\n", " 'xs': xs_datetime,\n", " 'ys': ys\n", "})\n", "\n", "p = (ggplot(data = df,\n", " mapping=aes('xs', 'ys'))\n", " + geom_vline(\n", " xintercept=pd.to_datetime('2025-10-26 00:59:59', utc=True).tz_convert('Europe/Berlin'),\n", " tooltips=layer_tooltips()\n", " .title('Berlin \"fall back\" transition')\n", " .line('^xintercept').format('^xintercept', '%b %d, %Y %H:%M:%S')\n", " .line('3:00 AM → 2:00 AM'),\n", " linetype='dotted', size=1.5)\n", " + geom_line(\n", " tooltips=layer_tooltips()\n", " .line('@xs')\n", " .format('^x', '%b %d, %Y %H:%M'),\n", " color='#1380A1', size=2)\n", " + ggtb() + ggsize(800, 400)) + theme(axis_title='blank')\n", "\n", "p + coord_cartesian(\n", " xlim=[\n", " pd.Timestamp('2025-10-25 22:00:00', tz='Europe/Berlin'),\n", " pd.Timestamp('2025-10-26 08:00:00', tz='Europe/Berlin')\n", " ]\n", ")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.9.23" } }, "nbformat": 4, "nbformat_minor": 5 }