{ "cells": [ { "cell_type": "markdown", "id": "8cbefec0-ed01-4e7f-a970-5c4be36154a6", "metadata": { "slideshow": { "slide_type": "skip" }, "tags": [] }, "source": [ "**NOTE: GitHub doesn't render the GIF animations in this notebook. View this locally or in nbviewer for best results.**\n", "\n", "[![Nbviewer](https://img.shields.io/badge/render-nbviewer-lightgrey?logo=jupyter)](https://nbviewer.jupyter.org/github/stefmolin/python-data-viz-workshop/blob/main/notebooks/2-animations.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/stefmolin/python-data-viz-workshop/main?urlpath=lab/tree/notebooks/2-animations.ipynb) [![View slides in browser](https://img.shields.io/badge/view-slides-orange?logo=reveal.js&logoColor=white)](https://stefaniemolin.com/python-data-viz-workshop/#/section-2)\n", "\n", "---" ] }, { "cell_type": "markdown", "id": "a0745604-3c71-4c7f-8b5c-b6cc0ecec3c3", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "# Section 2: Moving Beyond Static Visualizations\n", "\n", "Static visualizations are limited in how much information they can show. To move beyond these limitations, we can create animated and/or interactive visualizations. Animations make it possible for our visualizations to tell a story through movement of the plot components (e.g., bars, points, lines). Interactivity makes it possible to explore the data visually by hiding and displaying information based on user interest. In this section, we will focus on creating animated visualizations using Matplotlib before moving on to create interactive visualizations in the next section." ] }, { "cell_type": "markdown", "id": "9848c0e0-b1df-4273-b37a-c155f69db4e0", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "## Animating cumulative values over time\n", "\n", "In the previous section, we made a couple of visualizations to help us understand the number of Stack Overflow questions per library and how it changed over time. However, each of these came with some limitations." ] }, { "cell_type": "markdown", "id": "70c86a3e-d3df-4918-bcf1-60d2c1fdd482", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "We made a bar plot that captured the total number of questions per library, but it couldn't show us the growth in pandas questions over time (or how the growth rate changed over time):\n", "\n", "
\n", " \"bar\n", "
" ] }, { "cell_type": "markdown", "id": "e309814a-e109-43b9-9503-89ca99c0ad0d", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "We also made an area plot showing the number of questions per day over time for the top 4 libraries, but by limiting the libraries shown we lost some information:\n", "\n", "
\n", " \"area\n", "
" ] }, { "cell_type": "markdown", "id": "c83940b0-e41d-4333-9c7f-7b04f201e097", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Both of these visualizations gave us insight into the dataset. For example, we could see that pandas has by far the largest number of questions and has been growing at a faster rate than the other libraries. While this comes from studying the plots, an animation would make this much more obvious and, at the same time, capture the exponential growth in pandas questions that helped pandas overtake both Matplotlib and NumPy in cumulative questions." ] }, { "cell_type": "markdown", "id": "4a920e7f-b708-4e41-a7ae-d9b1156bbfc2", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Let's use Matplotlib to create an animated bar plot of cumulative questions over time to show this. We will do so in the following steps:\n", "1. Create a dataset of cumulative questions per library over time.\n", "2. Import the `FuncAnimation` class.\n", "3. Write a function for generating the initial plot.\n", "4. Write a function for generating annotations and plot text.\n", "5. Define the plot update function.\n", "6. Bind arguments to the update function.\n", "7. Animate the plot." ] }, { "cell_type": "markdown", "id": "ae02ff62-58ea-45ae-9f18-0a315c1d66ac", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 1. Create a dataset of cumulative questions per library over time.\n", "We will start by reading in our Stack Overflow dataset, but this time, we will calculate the total number of questions per month and then calculate the cumulative value over time:" ] }, { "cell_type": "code", "execution_count": 1, "id": "d9587e2a-2770-40dd-a096-f584da84529e", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
pandasmatplotlibnumpyseaborngeopandasgeoviewsaltairyellowbrickvegaholoviewshvplotbokeh
2021-05-31200734.057853.089812.06855.01456.057.0716.046.0532.0513.084.04270.0
2021-06-30205065.058602.091026.07021.01522.057.0760.048.0557.0521.088.04308.0
2021-07-31209235.059428.092254.07174.01579.062.0781.050.0572.0528.089.04341.0
2021-08-31213410.060250.093349.07344.01631.062.0797.052.0589.0541.092.04372.0
2021-09-30214919.060554.093797.07414.01652.063.0804.054.0598.0542.092.04386.0
\n", "
" ], "text/plain": [ " pandas matplotlib numpy seaborn geopandas geoviews \\\n", "2021-05-31 200734.0 57853.0 89812.0 6855.0 1456.0 57.0 \n", "2021-06-30 205065.0 58602.0 91026.0 7021.0 1522.0 57.0 \n", "2021-07-31 209235.0 59428.0 92254.0 7174.0 1579.0 62.0 \n", "2021-08-31 213410.0 60250.0 93349.0 7344.0 1631.0 62.0 \n", "2021-09-30 214919.0 60554.0 93797.0 7414.0 1652.0 63.0 \n", "\n", " altair yellowbrick vega holoviews hvplot bokeh \n", "2021-05-31 716.0 46.0 532.0 513.0 84.0 4270.0 \n", "2021-06-30 760.0 48.0 557.0 521.0 88.0 4308.0 \n", "2021-07-31 781.0 50.0 572.0 528.0 89.0 4341.0 \n", "2021-08-31 797.0 52.0 589.0 541.0 92.0 4372.0 \n", "2021-09-30 804.0 54.0 598.0 542.0 92.0 4386.0 " ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "\n", "questions_per_library = pd.read_csv(\n", " '../data/stackoverflow.zip', parse_dates=True, index_col='creation_date'\n", ").loc[:,'pandas':'bokeh'].resample('1ME').sum().cumsum().reindex(\n", " pd.date_range('2008-08', '2021-10', freq='ME')\n", ").fillna(0)\n", "questions_per_library.tail()" ] }, { "cell_type": "markdown", "id": "8df9bcd1-ac2a-4b23-b512-18e7f40b1b65", "metadata": { "tags": [] }, "source": [ "*Source: [Stack Exchange Network](https://api.stackexchange.com/docs/search)*" ] }, { "cell_type": "markdown", "id": "3a9e23ed-d754-4832-83ad-3d3dd16ef597", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 2. Import the `FuncAnimation` class.\n", "To create animations with Matplotlib, we will be using the `FuncAnimation` class, so let's import it now:" ] }, { "cell_type": "code", "execution_count": 2, "id": "dda8851c-700f-4fa2-afb6-db83a1fbce2f", "metadata": {}, "outputs": [], "source": [ "from matplotlib.animation import FuncAnimation" ] }, { "cell_type": "markdown", "id": "b0efc2d0-231e-4873-a93b-43258d0f3c42", "metadata": { "slideshow": { "slide_type": "fragment" }, "tags": [] }, "source": [ "At a minimum, we will need to provide the following when instantiating a `FuncAnimation` object:\n", "- The `Figure` object to draw on.\n", "- A function to call at each frame to update the plot." ] }, { "cell_type": "markdown", "id": "9c7781f3-9957-4db9-8b00-eff518d755aa", "metadata": { "slideshow": { "slide_type": "fragment" }, "tags": [] }, "source": [ "In the next few steps, we will work on the logic for these." ] }, { "cell_type": "markdown", "id": "e2c93b57-a9c9-4fa5-8ac7-20e4b3aaba22", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 3. Write a function for generating the initial plot.\n", "Since we are required to pass in a `Figure` object and bake all the plot update logic into a function, we will start by building up an initial plot. Here, we create a bar plot with bars of width 0, so that they don't show up for now. The y-axis is set up so that the libraries with the most questions overall are at the top:" ] }, { "cell_type": "code", "execution_count": 3, "id": "93388439-359b-43eb-8be9-462e49babc97", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from matplotlib import ticker\n", "from utils import despine\n", "\n", "\n", "def bar_plot(data):\n", " fig, ax = plt.subplots(figsize=(6, 4), layout='constrained')\n", " sort_order = data.loc[data.index.max()].sort_values().index\n", " bars = ax.barh(sort_order, [0] * data.shape[1], label=sort_order)\n", " \n", " ax.set_xlabel('total questions', fontweight='bold')\n", " ax.set_xlim(0, 250_000)\n", " ax.xaxis.set_major_formatter(ticker.EngFormatter())\n", " ax.xaxis.set_tick_params(labelsize=11)\n", " ax.yaxis.set_tick_params(labelsize=11)\n", " despine(ax)\n", "\n", " return fig, ax" ] }, { "cell_type": "markdown", "id": "7d6bdfe5-847e-48eb-acc5-56cadb592d1d", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "This gives us a plot that we can update:" ] }, { "cell_type": "code", "execution_count": 4, "id": "48ecdd79-3acc-459e-94e5-455631ae16e3", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(
, )" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " (c) 2021-2024 Stefanie Molin\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.8.4, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import matplotlib_inline\n", "from utils import mpl_svg_config\n", "\n", "matplotlib_inline.backend_inline.set_matplotlib_formats(\n", " 'svg', **mpl_svg_config('section-2')\n", ")\n", "bar_plot(questions_per_library)" ] }, { "cell_type": "markdown", "id": "aeb78cde-6309-43cf-860f-70fd0aa2a771", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 4. Write a function for generating annotations and plot text.\n", "\n", "We will also need to initialize annotations for each of the bars and some text to show the date in the animation (month and year):" ] }, { "cell_type": "code", "execution_count": 5, "id": "ff920c16-4bd6-4eed-ae3b-91477dbd07a5", "metadata": {}, "outputs": [], "source": [ "def generate_plot_text(ax):\n", " annotations = [\n", " ax.annotate(\n", " '', xy=(0, bar.get_y() + bar.get_height() / 2), ha='left', va='center'\n", " )\n", " for bar in ax.patches\n", " ]\n", "\n", " time_text = ax.text(\n", " 0.9, 0.1, '', transform=ax.transAxes, fontsize=15, ha='center', va='center'\n", " )\n", " return annotations, time_text" ] }, { "cell_type": "markdown", "id": "89b89048-2a6e-4d80-b0ae-2d6db322d750", "metadata": {}, "source": [ "*Tip: We are passing in `transform=ax.transAxes` when we place our time text in order to specify the location in terms of the `Axes` object's coordinates instead of basing it off the data in the plot so that it is easier to place.*" ] }, { "cell_type": "markdown", "id": "8de353ab-dc05-45c1-b49a-749234181611", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 5. Define the plot update function.\n", "\n", "Next, we will make our plot update function. This will be called at each frame. We will extract that frame's data (the cumulative questions for that month), and then update the width of each of the bars. In addition, we will annotate the bars if their widths are greater than 0. At every frame, we will also need to update our time annotation (`time_text`):" ] }, { "cell_type": "code", "execution_count": 6, "id": "fff5e12e-c67b-46b4-a7bb-797cc4ac0b25", "metadata": {}, "outputs": [], "source": [ "def update(frame, *, ax, df, annotations, time_text):\n", " data = df.loc[frame, :]\n", " \n", " # update bars\n", " for rect, text in zip(ax.patches, annotations):\n", " col = rect.get_label()\n", " if data[col]:\n", " rect.set_width(data[col])\n", " text.set_x(data[col])\n", " text.set_text(f' {data[col]:,.0f}')\n", "\n", " # update time\n", " time_text.set_text(frame.strftime('%b\\n%Y'))" ] }, { "cell_type": "markdown", "id": "cc7b10e0-a660-45b7-8071-4d997665a05e", "metadata": {}, "source": [ "*Tip: The asterisk in the function signature requires all arguments after it to be passed in by name. This makes sure that we explicitly define the components for the animation when calling the function. Read more on this syntax [here](https://www.python.org/dev/peps/pep-3102/).*" ] }, { "cell_type": "markdown", "id": "0d0bbb34-1996-42d7-b75a-ebbebf98823c", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 6. Bind arguments to the update function.\n", "\n", "The last step before creating our animation is to create a function that will assemble everything we need to pass to `FuncAnimation`. Note that our `update()` function requires multiple parameters, but we would be passing in the same values every time (since we would only change the value for `frame`). To make this simpler, we create a [partial function](https://docs.python.org/3/library/functools.html#functools.partial), which **binds** values to each of those arguments so that we only have to pass in `frame` when we call the partial. This is essentially a [closure](https://www.programiz.com/python-programming/closure), where `bar_plot_init()` is the enclosing function and `update()` is the nested function, which we defined in the previous code block for readability:" ] }, { "cell_type": "code", "execution_count": 7, "id": "78a7425a-8592-40f0-af88-c389bd3d817b", "metadata": {}, "outputs": [], "source": [ "from functools import partial\n", "\n", "def bar_plot_init(questions_per_library):\n", " fig, ax = bar_plot(questions_per_library)\n", " annotations, time_text = generate_plot_text(ax)\n", "\n", " bar_plot_update = partial(\n", " update, ax=ax, df=questions_per_library,\n", " annotations=annotations, time_text=time_text\n", " )\n", "\n", " return fig, bar_plot_update" ] }, { "cell_type": "markdown", "id": "a69e2c37-01e0-4f60-89fd-04cea1477b4c", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 7. Animate the plot.\n", "Finally, we are ready to create our animation. We start by calling the `bar_plot_init()` function from the previous code block to generate the `Figure` object and partial function for the update of the plot. Then, we pass in the `Figure` object and update function when initializing our `FuncAnimation` object. We also specify the `frames` argument as the index of our DataFrame (the dates) and that the animation shouldn't repeat because we will save it as an MP4 video:" ] }, { "cell_type": "code", "execution_count": 8, "id": "506ee511-d464-403a-b059-5b4d31b9848e", "metadata": { "tags": [] }, "outputs": [], "source": [ "fig, update_func = bar_plot_init(questions_per_library)\n", "\n", "ani = FuncAnimation(\n", " fig, update_func, frames=questions_per_library.index, repeat=False\n", ")\n", "ani.save(\n", " '../media/stackoverflow_questions.mp4', \n", " writer='ffmpeg', fps=10, bitrate=100, dpi=300\n", ")\n", "plt.close()" ] }, { "cell_type": "markdown", "id": "8632ed82-75d1-4b0e-8951-4621782ab385", "metadata": {}, "source": [ "**Important**: The `FuncAnimation` object **must** be assigned to a variable when creating it; otherwise, without any references to it, Python will garbage collect it – ending the animation. For more information on garbage collection in Python, check out [this](https://stackify.com/python-garbage-collection/) article." ] }, { "cell_type": "markdown", "id": "26562906-49db-4033-b900-45742205b32a", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Now, let's view the animation we just saved as an MP4 file:" ] }, { "cell_type": "code", "execution_count": 9, "id": "012d9500-f04d-4dd1-81a1-f479d6a4ecff", "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython import display\n", "\n", "display.Video(\n", " '../media/stackoverflow_questions.mp4', width=600, height=400,\n", " embed=True, html_attributes='controls muted autoplay'\n", ")" ] }, { "cell_type": "markdown", "id": "8f0682ff-d1ef-49d5-8374-d37a6362ca8a", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "## Animating distributions over time\n", "\n", "As with the previous example, the histograms of daily Manhattan subway entries in 2018 (from the first section of the workshop) don't tell the whole story of the dataset because the distributions changed drastically in 2020 and 2021:\n", "\n", "
\n", " \"Histograms\n", "
" ] }, { "cell_type": "markdown", "id": "e680b062-9249-4cd3-b9aa-6eb33700de1e", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "We will make an animated version of these histograms that enables us to see the distributions changing over time. Note that this example will have two key differences from the previous one. The first is that we will be animating subplots rather than a single plot, and the second is that we will use a technique called **blitting** to only update the portion of the subplots that has changed. This requires that we return the [*artists*](https://matplotlib.org/stable/tutorials/intermediate/artists.html) that need to be redrawn in the plot update function." ] }, { "cell_type": "markdown", "id": "47416264-2920-4c3f-aada-1e93333b6430", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "To make this visualization, we will work through these steps:\n", "\n", "1. Create a dataset of daily subway entries.\n", "2. Determine the bin ranges for the histograms.\n", "3. Write a function for generating the initial histogram subplots.\n", "4. Write a function for generating an annotation for the time period.\n", "5. Define the plot update function.\n", "6. Bind arguments for the update function.\n", "7. Animate the plot." ] }, { "cell_type": "markdown", "id": "41330122-773d-444f-befd-76af153b64dd", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 1. Create a dataset of daily subway entries.\n", "As we did previously, we will read in the subway dataset, which contains the total entries and exits per day per borough:" ] }, { "cell_type": "code", "execution_count": 10, "id": "5b9baa49-289f-4c92-b024-f349b9ca974c", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
EntriesExits
BoroughBkBxMQBkBxMQ
Datetime
2017-02-04617650.0247539.01390496.0408736.0417449.0148237.01225689.0279699.0
2017-02-05542667.0199078.01232537.0339716.0405607.0139856.01033610.0268626.0
2017-02-061184916.0472846.02774016.0787206.0761166.0267991.02240027.0537780.0
2017-02-071192638.0470573.02892462.0790557.0763653.0270007.02325024.0544828.0
2017-02-081243658.0497412.02998897.0825679.0788356.0275695.02389534.0559639.0
\n", "
" ], "text/plain": [ " Entries Exits \\\n", "Borough Bk Bx M Q Bk Bx \n", "Datetime \n", "2017-02-04 617650.0 247539.0 1390496.0 408736.0 417449.0 148237.0 \n", "2017-02-05 542667.0 199078.0 1232537.0 339716.0 405607.0 139856.0 \n", "2017-02-06 1184916.0 472846.0 2774016.0 787206.0 761166.0 267991.0 \n", "2017-02-07 1192638.0 470573.0 2892462.0 790557.0 763653.0 270007.0 \n", "2017-02-08 1243658.0 497412.0 2998897.0 825679.0 788356.0 275695.0 \n", "\n", " \n", "Borough M Q \n", "Datetime \n", "2017-02-04 1225689.0 279699.0 \n", "2017-02-05 1033610.0 268626.0 \n", "2017-02-06 2240027.0 537780.0 \n", "2017-02-07 2325024.0 544828.0 \n", "2017-02-08 2389534.0 559639.0 " ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "subway = pd.read_csv(\n", " '../data/NYC_subway_daily.csv', parse_dates=['Datetime'], \n", " index_col=['Borough', 'Datetime']\n", ")\n", "subway_daily = subway.unstack(0)\n", "subway_daily.head()" ] }, { "cell_type": "markdown", "id": "72aef4f7-6254-41ee-99bf-a8478131b258", "metadata": { "tags": [] }, "source": [ "*Source: The above dataset was resampled from [this](https://www.kaggle.com/eddeng/nyc-subway-traffic-data-20172021?select=NYC_subway_traffic_2017-2021.csv) dataset provided by Kaggle user [Edden](https://www.kaggle.com/eddeng).*" ] }, { "cell_type": "markdown", "id": "8524f0c6-5c77-4708-a03d-5628025a13ba", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "For this visualization, we will just be working with the entries in Manhattan:" ] }, { "cell_type": "code", "execution_count": 11, "id": "8f6601fa-721b-4c6c-9b57-d3489e83cf69", "metadata": {}, "outputs": [], "source": [ "manhattan_entries = subway_daily['Entries']['M']" ] }, { "cell_type": "markdown", "id": "42b9c1f1-de5c-4a8f-ad3d-b344423a28de", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 2. Determine the bin ranges for the histograms.\n", "Before we can set up the subplots, we have to calculate the bin ranges for the histograms so that our animation is smooth. NumPy provides the `histogram()` function, which gives us both the number of data points in each bin and the bin ranges, respectively. We will also be using this function to update the histograms during the animation:" ] }, { "cell_type": "code", "execution_count": 12, "id": "3312366c-4f28-4ffb-9ff9-7e4786cd6bf1", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "count_per_bin, bin_ranges = np.histogram(manhattan_entries, bins=30)" ] }, { "cell_type": "markdown", "id": "31b7ec05-816e-4843-9b41-702e9cc288cc", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 3. Write a function for generating the initial histogram subplots.\n", "\n", "Next, we will handle the logic for building our initial histogram, packaging it in a function:" ] }, { "cell_type": "code", "execution_count": 13, "id": "43f715cf-b6da-412a-85e8-4d589d492ba3", "metadata": {}, "outputs": [], "source": [ "def subway_histogram(data, bins, date_range):\n", " _, bin_ranges = np.histogram(data, bins=bins)\n", "\n", " weekday_mask = data.index.weekday < 5\n", " configs = [\n", " {'label': 'Weekend', 'mask': ~weekday_mask, 'ymax': 60},\n", " {'label': 'Weekday', 'mask': weekday_mask, 'ymax': 120}\n", " ]\n", " \n", " fig, axes = plt.subplots(1, 2, figsize=(6, 3), sharex=True, layout='constrained')\n", " for ax, config in zip(axes, configs):\n", " _, _, config['hist'] = ax.hist(\n", " data[config['mask']].loc[date_range], bin_ranges, ec='black'\n", " )\n", " ax.xaxis.set_major_formatter(ticker.EngFormatter())\n", " ax.set(\n", " xlim=(0, None), ylim=(0, config['ymax']),\n", " xlabel=f'{config[\"label\"]} Entries'\n", " )\n", " despine(ax)\n", "\n", " axes[0].set_ylabel('Frequency')\n", " fig.suptitle('Histogram of Daily Subway Entries in Manhattan')\n", "\n", " return fig, axes, bin_ranges, configs" ] }, { "cell_type": "markdown", "id": "cad233ed-1ced-462c-810b-9efbab67bff4", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Notice that our plot this time starts out with data already – this is because we want to show the change in the distribution of daily entries in the last year:" ] }, { "cell_type": "code", "execution_count": 14, "id": "cade0ffe-5988-4368-be77-4a99a7968b61", "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " (c) 2021-2024 Stefanie Molin\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.8.4, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "_ = subway_histogram(manhattan_entries, bins=30, date_range='2017')" ] }, { "cell_type": "markdown", "id": "ee5765b6-be38-4417-9964-e315981e2e31", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 4. Write a function for generating an annotation for the time period.\n", "\n", "We will once again include some text that indicates the time period as the animation runs. This is similar to what we had in the previous example:" ] }, { "cell_type": "code", "execution_count": 15, "id": "976f7412-bd07-4c7b-8cf0-ef51f4be056a", "metadata": {}, "outputs": [], "source": [ "def add_time_text(ax):\n", " time_text = ax.text(\n", " 0.15, 0.9, '', transform=ax.transAxes,\n", " fontsize=12, ha='center', va='center'\n", " )\n", " return time_text" ] }, { "cell_type": "markdown", "id": "115981f0-2b8d-4550-8fb6-ffb700bef626", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 5. Define the plot update function.\n", "\n", "Now, we will create our update function. This time, we have to update both subplots and return any artists that need to be redrawn since we are going to use blitting:" ] }, { "cell_type": "code", "execution_count": 16, "id": "e3bf05ef-8364-411a-a871-bbe6bead1cb1", "metadata": {}, "outputs": [], "source": [ "def update(frame, *, data, configs, time_text, bin_ranges):\n", " artists = []\n", "\n", " time = frame.strftime('%b\\n%Y')\n", " if time != time_text.get_text():\n", " time_text.set_text(time)\n", " artists.append(time_text)\n", "\n", " for config in configs:\n", " time_frame_mask = \\\n", " (data.index > frame - pd.Timedelta(days=365)) & (data.index <= frame)\n", " counts, _ = np.histogram(\n", " data[time_frame_mask & config['mask']],\n", " bin_ranges\n", " )\n", " for count, rect in zip(counts, config['hist'].patches):\n", " if count != rect.get_height():\n", " rect.set_height(count)\n", " artists.append(rect)\n", "\n", " return artists" ] }, { "cell_type": "markdown", "id": "92045ca0-a9b4-4585-8ce6-b43fa6b0c7ab", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 6. Bind arguments for the update function.\n", "\n", "As our final step before generating the animation, we bind our arguments to the update function using a partial function:" ] }, { "cell_type": "code", "execution_count": 17, "id": "2c26f024-1db6-4017-a25c-c4d3930e0020", "metadata": {}, "outputs": [], "source": [ "def histogram_init(data, bins, initial_date_range):\n", " fig, axes, bin_ranges, configs = subway_histogram(data, bins, initial_date_range)\n", "\n", " update_func = partial(\n", " update, data=data, configs=configs,\n", " time_text=add_time_text(axes[0]),\n", " bin_ranges=bin_ranges\n", " )\n", "\n", " return fig, update_func" ] }, { "cell_type": "markdown", "id": "fb3a3648-63a0-40b2-9a18-995de3c547bf", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 7. Animate the plot.\n", "Finally, we will animate the plot using `FuncAnimation` like before. Notice that this time we are passing in `blit=True`, so that only the artists that we returned in the `update()` function are redrawn. We are specifying to make updates for each day in the data starting on August 1, 2019:" ] }, { "cell_type": "code", "execution_count": 18, "id": "bbea2d82-a2a9-4de4-8396-6d7d7dfd7435", "metadata": {}, "outputs": [], "source": [ "fig, update_func = histogram_init(\n", " manhattan_entries, bins=30, initial_date_range=slice('2017', '2019-07')\n", ")\n", "\n", "ani = FuncAnimation(\n", " fig, update_func, frames=manhattan_entries['2019-08':'2021'].index,\n", " repeat=False, blit=True\n", ")\n", "ani.save(\n", " '../media/subway_entries_subplots.mp4',\n", " writer='ffmpeg', fps=30, bitrate=500, dpi=300\n", ")\n", "plt.close()" ] }, { "cell_type": "markdown", "id": "8c4ddac2-8fb9-4a95-8ee6-45c90f0dfa20", "metadata": {}, "source": [ "*Tip: We are using a `slice` object to pass a date range for pandas to use with `loc[]`. More information on `slice()` can be found [here](https://docs.python.org/3/library/functions.html?highlight=slice#slice).*" ] }, { "cell_type": "markdown", "id": "fe7ce75e-53fc-4041-83be-424b1ce35e2e", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Our animation makes it easy to see the change in the distributions over time:" ] }, { "cell_type": "code", "execution_count": 19, "id": "bf3174b8-02d4-4577-b29a-d7206a3825e8", "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython import display\n", "\n", "display.Video(\n", " '../media/subway_entries_subplots.mp4', width=600, height=300,\n", " embed=True, html_attributes='controls muted autoplay'\n", ")" ] }, { "cell_type": "markdown", "id": "9783fc2e-5068-4b13-8d0c-3a93b1d8c41a", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "### [Exercise 2.1](./workbook.ipynb#Exercise-2.1)\n", "\n", "##### Modify the animation of daily subway entries to show both the weekday and weekend histograms on the same subplot (you only need one now). Don't forget to change the transparency of the bars to be able to visualize the overlap." ] }, { "cell_type": "code", "execution_count": 20, "id": "56e434f9-d1a7-4f08-8c26-93cec91a2ae7", "metadata": {}, "outputs": [], "source": [ "# Complete this exercise in the `workbook.ipynb` file" ] }, { "cell_type": "code", "execution_count": 21, "id": "4dcde9f9-063c-47e0-9aef-b0414d61a105", "metadata": {}, "outputs": [], "source": [ "# Click on `Exercise 2.1` above to open the `workbook.ipynb` file" ] }, { "cell_type": "code", "execution_count": 22, "id": "58f90bf2-74c1-4c37-a34b-04fce2b955d6", "metadata": { "tags": [] }, "outputs": [], "source": [ "#" ] }, { "cell_type": "code", "execution_count": 23, "id": "dcf97445-aa15-440c-a0c6-17ccd4b2aab4", "metadata": { "tags": [] }, "outputs": [], "source": [ "#" ] }, { "cell_type": "code", "execution_count": 24, "id": "b6c1a1ee-d5d6-4e55-8558-a2cc4c6a544a", "metadata": {}, "outputs": [], "source": [ "# WARNING: if you complete the exercise here, your cell numbers\n", "# for the rest of the workshop might not match the slides" ] }, { "cell_type": "code", "execution_count": 25, "id": "ac269033-5825-4d2d-a09f-3376f28e0d9b", "metadata": {}, "outputs": [], "source": [ "#" ] }, { "cell_type": "code", "execution_count": 26, "id": "ec448f08-913e-4c29-aa52-b8ab9db411bc", "metadata": {}, "outputs": [], "source": [ "# " ] }, { "cell_type": "code", "execution_count": 27, "id": "cae6cfe2-fc85-4d7d-b2aa-897fd7239bfa", "metadata": {}, "outputs": [], "source": [ "# TIP: the `despine()` function is available in the `utils.py` file" ] }, { "cell_type": "markdown", "id": "f19c7af6-6ff0-4caf-adc0-aa26583b3e5c", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "## Animating geospatial data with HoloViz\n", "\n", "[HoloViz](https://holoviz.org/) provides multiple high-level tools that aim to simplify data visualization in Python. For this example, we will be looking at [HoloViews](https://holoviews.org/) and [GeoViews](https://geoviews.org/), which extends HoloViews for use with geographic data. HoloViews abstracts away some of the plotting logic, removing boilerplate code and making it possible to easily switch backends (e.g., switch from Matplotlib to Bokeh for JavaScript-powered, interactive plotting). To wrap up our discussion on animation, we will use GeoViews to create an animation of earthquakes per month in 2020 on a map of the world." ] }, { "cell_type": "markdown", "id": "6f8067e2-d309-4f5a-a122-6d7db4958ef9", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "To make this visualization, we will work through the following steps:\n", "1. Use GeoPandas to read in our data.\n", "2. Handle HoloViz imports and set up the Matplotlib backend.\n", "3. Define a function for plotting earthquakes on a map using GeoViews.\n", "4. Create a mapping of frames to plots using HoloViews.\n", "5. Animate the plot." ] }, { "cell_type": "markdown", "id": "7bd1a315-8b75-4993-80e3-298948c8b1bf", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 1. Use GeoPandas to read in our data.\n", "Our dataset is in GeoJSON format, so the best way to read it in will be to use [GeoPandas](https://geopandas.org/), which is a library that makes working with geospatial data in Python easier. It builds on top of pandas, so we don't have to learn any additional syntax for this example." ] }, { "cell_type": "markdown", "id": "4e795a5a-0d93-4795-a3b8-f5e9e5f72730", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Here, we import GeoPandas and then use the `read_file()` function to read the earthquakes GeoJSON data into a `GeoDataFrame` object:" ] }, { "cell_type": "code", "execution_count": 28, "id": "3bdf3d8c-78f2-46e7-a11d-c34c124d2f6b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(188527, 4)" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import geopandas as gpd\n", "\n", "earthquakes = gpd.read_file('../data/earthquakes.geojson').assign(\n", " time=lambda x: pd.to_datetime(x.time, unit='ms'),\n", " month=lambda x: x.time.dt.month\n", ")[['geometry', 'mag', 'time', 'month']]\n", "\n", "earthquakes.shape" ] }, { "cell_type": "markdown", "id": "35dbef3b-166a-458d-b352-38289b8fb61b", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Our data looks like this:" ] }, { "cell_type": "code", "execution_count": 29, "id": "07d20928-d596-41f0-b9d2-64abe1d16f78", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
geometrymagtimemonth
0POINT Z (-67.12750 19.21750 12.00000)2.752020-01-01 00:01:56.5901
1POINT Z (-67.09010 19.07660 6.00000)2.552020-01-01 00:03:38.2101
2POINT Z (-66.85410 17.87050 6.00000)1.812020-01-01 00:05:09.4401
3POINT Z (-66.86360 17.89930 8.00000)1.842020-01-01 00:05:36.9301
4POINT Z (-66.86850 17.90660 8.00000)1.642020-01-01 00:09:20.0601
\n", "
" ], "text/plain": [ " geometry mag time month\n", "0 POINT Z (-67.12750 19.21750 12.00000) 2.75 2020-01-01 00:01:56.590 1\n", "1 POINT Z (-67.09010 19.07660 6.00000) 2.55 2020-01-01 00:03:38.210 1\n", "2 POINT Z (-66.85410 17.87050 6.00000) 1.81 2020-01-01 00:05:09.440 1\n", "3 POINT Z (-66.86360 17.89930 8.00000) 1.84 2020-01-01 00:05:36.930 1\n", "4 POINT Z (-66.86850 17.90660 8.00000) 1.64 2020-01-01 00:09:20.060 1" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "earthquakes.head()" ] }, { "cell_type": "markdown", "id": "b660feb3-9a82-458c-88ea-6cea82f1b1a8", "metadata": {}, "source": [ "*Source: [USGS API](https://earthquake.usgs.gov/fdsnws/event/1/)*" ] }, { "cell_type": "markdown", "id": "4cfc6ef0-ac2a-4f26-9db0-ef58e595bec3", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 2. Handle HoloViz imports and set up the Matplotlib backend.\n", "\n", "Since our earthquakes dataset contains geometries, we will use GeoViews in addition to HoloViews to create our animation. For this example, we will be using the [Matplotlib backend](http://holoviews.org/user_guide/Plotting_with_Matplotlib.html):" ] }, { "cell_type": "code", "execution_count": 30, "id": "a09f6ebb-b0fd-4538-97ec-3b204a0cc022", "metadata": {}, "outputs": [ { "data": { "application/javascript": [ "(function(root) {\n", " function now() {\n", " return new Date();\n", " }\n", "\n", " var force = true;\n", " var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", " var reloading = false;\n", " var Bokeh = root.Bokeh;\n", "\n", " if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n", " root._bokeh_timeout = Date.now() + 5000;\n", " root._bokeh_failed_load = false;\n", " }\n", "\n", " function run_callbacks() {\n", " try {\n", " root._bokeh_onload_callbacks.forEach(function(callback) {\n", " if (callback != null)\n", " callback();\n", " });\n", " } finally {\n", " delete root._bokeh_onload_callbacks;\n", " }\n", " console.debug(\"Bokeh: all callbacks have finished\");\n", " }\n", "\n", " function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n", " if (css_urls == null) css_urls = [];\n", " if (js_urls == null) js_urls = [];\n", " if (js_modules == null) js_modules = [];\n", " if (js_exports == null) js_exports = {};\n", "\n", " root._bokeh_onload_callbacks.push(callback);\n", "\n", " if (root._bokeh_is_loading > 0) {\n", " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", " return null;\n", " }\n", " if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", " run_callbacks();\n", " return null;\n", " }\n", " if (!reloading) {\n", " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", " }\n", "\n", " function on_load() {\n", " root._bokeh_is_loading--;\n", " if (root._bokeh_is_loading === 0) {\n", " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", " run_callbacks()\n", " }\n", " }\n", " window._bokeh_on_load = on_load\n", "\n", " function on_error() {\n", " console.error(\"failed to load \" + url);\n", " }\n", "\n", " var skip = [];\n", " if (window.requirejs) {\n", " window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n", " root._bokeh_is_loading = css_urls.length + 0;\n", " } else {\n", " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", " }\n", "\n", " var existing_stylesheets = []\n", " var links = document.getElementsByTagName('link')\n", " for (var i = 0; i < links.length; i++) {\n", " var link = links[i]\n", " if (link.href != null) {\n", "\texisting_stylesheets.push(link.href)\n", " }\n", " }\n", " for (var i = 0; i < css_urls.length; i++) {\n", " var url = css_urls[i];\n", " if (existing_stylesheets.indexOf(url) !== -1) {\n", "\ton_load()\n", "\tcontinue;\n", " }\n", " const element = document.createElement(\"link\");\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.rel = \"stylesheet\";\n", " element.type = \"text/css\";\n", " element.href = url;\n", " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", " document.body.appendChild(element);\n", " } var existing_scripts = []\n", " var scripts = document.getElementsByTagName('script')\n", " for (var i = 0; i < scripts.length; i++) {\n", " var script = scripts[i]\n", " if (script.src != null) {\n", "\texisting_scripts.push(script.src)\n", " }\n", " }\n", " for (var i = 0; i < js_urls.length; i++) {\n", " var url = js_urls[i];\n", " if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n", "\tif (!window.requirejs) {\n", "\t on_load();\n", "\t}\n", "\tcontinue;\n", " }\n", " var element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.src = url;\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " document.head.appendChild(element);\n", " }\n", " for (var i = 0; i < js_modules.length; i++) {\n", " var url = js_modules[i];\n", " if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n", "\tif (!window.requirejs) {\n", "\t on_load();\n", "\t}\n", "\tcontinue;\n", " }\n", " var element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.src = url;\n", " element.type = \"module\";\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " document.head.appendChild(element);\n", " }\n", " for (const name in js_exports) {\n", " var url = js_exports[name];\n", " if (skip.indexOf(url) >= 0 || root[name] != null) {\n", "\tif (!window.requirejs) {\n", "\t on_load();\n", "\t}\n", "\tcontinue;\n", " }\n", " var element = document.createElement('script');\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.type = \"module\";\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " element.textContent = `\n", " import ${name} from \"${url}\"\n", " window.${name} = ${name}\n", " window._bokeh_on_load()\n", " `\n", " document.head.appendChild(element);\n", " }\n", " if (!js_urls.length && !js_modules.length) {\n", " on_load()\n", " }\n", " };\n", "\n", " function inject_raw_css(css) {\n", " const element = document.createElement(\"style\");\n", " element.appendChild(document.createTextNode(css));\n", " document.body.appendChild(element);\n", " }\n", "\n", " var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.4.1.min.js\", \"https://cdn.holoviz.org/panel/1.4.2/dist/panel.min.js\"];\n", " var js_modules = [];\n", " var js_exports = {};\n", " var css_urls = [];\n", " var inline_js = [ function(Bokeh) {\n", " Bokeh.set_log_level(\"info\");\n", " },\n", "function(Bokeh) {} // ensure no trailing comma for IE\n", " ];\n", "\n", " function run_inline_js() {\n", " if ((root.Bokeh !== undefined) || (force === true)) {\n", " for (var i = 0; i < inline_js.length; i++) {\n", "\ttry {\n", " inline_js[i].call(root, root.Bokeh);\n", "\t} catch(e) {\n", "\t if (!reloading) {\n", "\t throw e;\n", "\t }\n", "\t}\n", " }\n", " // Cache old bokeh versions\n", " if (Bokeh != undefined && !reloading) {\n", "\tvar NewBokeh = root.Bokeh;\n", "\tif (Bokeh.versions === undefined) {\n", "\t Bokeh.versions = new Map();\n", "\t}\n", "\tif (NewBokeh.version !== Bokeh.version) {\n", "\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n", "\t}\n", "\troot.Bokeh = Bokeh;\n", " }} else if (Date.now() < root._bokeh_timeout) {\n", " setTimeout(run_inline_js, 100);\n", " } else if (!root._bokeh_failed_load) {\n", " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", " root._bokeh_failed_load = true;\n", " }\n", " root._bokeh_is_initializing = false\n", " }\n", "\n", " function load_or_wait() {\n", " // Implement a backoff loop that tries to ensure we do not load multiple\n", " // versions of Bokeh and its dependencies at the same time.\n", " // In recent versions we use the root._bokeh_is_initializing flag\n", " // to determine whether there is an ongoing attempt to initialize\n", " // bokeh, however for backward compatibility we also try to ensure\n", " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", " // before older versions are fully initialized.\n", " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", " root._bokeh_is_initializing = false;\n", " root._bokeh_onload_callbacks = undefined;\n", " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", " load_or_wait();\n", " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", " setTimeout(load_or_wait, 100);\n", " } else {\n", " root._bokeh_is_initializing = true\n", " root._bokeh_onload_callbacks = []\n", " var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n", " if (!reloading && !bokeh_loaded) {\n", "\troot.Bokeh = undefined;\n", " }\n", " load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n", "\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", "\trun_inline_js();\n", " });\n", " }\n", " }\n", " // Give older versions of the autoload script a head-start to ensure\n", " // they initialize before we start loading newer version.\n", " setTimeout(load_or_wait, 100)\n", "}(window));" ], "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.4.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = false;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.4.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.4.1.min.js\", \"https://cdn.holoviz.org/panel/1.4.2/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "\n", "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", "}\n", "\n", "\n", " function JupyterCommManager() {\n", " }\n", "\n", " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", " comm_manager.register_target(comm_id, function(comm) {\n", " comm.on_msg(msg_handler);\n", " });\n", " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", " comm.onMsg = msg_handler;\n", " });\n", " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", " var messages = comm.messages[Symbol.asyncIterator]();\n", " function processIteratorResult(result) {\n", " var message = result.value;\n", " console.log(message)\n", " var content = {data: message.data, comm_id};\n", " var buffers = []\n", " for (var buffer of message.buffers || []) {\n", " buffers.push(new DataView(buffer))\n", " }\n", " var metadata = message.metadata || {};\n", " var msg = {content, buffers, metadata}\n", " msg_handler(msg);\n", " return messages.next().then(processIteratorResult);\n", " }\n", " return messages.next().then(processIteratorResult);\n", " })\n", " }\n", " }\n", "\n", " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", " if (comm_id in window.PyViz.comms) {\n", " return window.PyViz.comms[comm_id];\n", " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", " if (msg_handler) {\n", " comm.on_msg(msg_handler);\n", " }\n", " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", " comm.open();\n", " if (msg_handler) {\n", " comm.onMsg = msg_handler;\n", " }\n", " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", " comm_promise.then((comm) => {\n", " window.PyViz.comms[comm_id] = comm;\n", " if (msg_handler) {\n", " var messages = comm.messages[Symbol.asyncIterator]();\n", " function processIteratorResult(result) {\n", " var message = result.value;\n", " var content = {data: message.data};\n", " var metadata = message.metadata || {comm_id};\n", " var msg = {content, metadata}\n", " msg_handler(msg);\n", " return messages.next().then(processIteratorResult);\n", " }\n", " return messages.next().then(processIteratorResult);\n", " }\n", " }) \n", " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", " return comm_promise.then((comm) => {\n", " comm.send(data, metadata, buffers, disposeOnDone);\n", " });\n", " };\n", " var comm = {\n", " send: sendClosure\n", " };\n", " }\n", " window.PyViz.comms[comm_id] = comm;\n", " return comm;\n", " }\n", " window.PyViz.comm_manager = new JupyterCommManager();\n", " \n", "\n", "\n", "var JS_MIME_TYPE = 'application/javascript';\n", "var HTML_MIME_TYPE = 'text/html';\n", "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", "var CLASS_NAME = 'output';\n", "\n", "/**\n", " * Render data to the DOM node\n", " */\n", "function render(props, node) {\n", " var div = document.createElement(\"div\");\n", " var script = document.createElement(\"script\");\n", " node.appendChild(div);\n", " node.appendChild(script);\n", "}\n", "\n", "/**\n", " * Handle when a new output is added\n", " */\n", "function handle_add_output(event, handle) {\n", " var output_area = handle.output_area;\n", " var output = handle.output;\n", " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", " return\n", " }\n", " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", " if (id !== undefined) {\n", " var nchildren = toinsert.length;\n", " var html_node = toinsert[nchildren-1].children[0];\n", " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", " var scripts = [];\n", " var nodelist = html_node.querySelectorAll(\"script\");\n", " for (var i in nodelist) {\n", " if (nodelist.hasOwnProperty(i)) {\n", " scripts.push(nodelist[i])\n", " }\n", " }\n", "\n", " scripts.forEach( function (oldScript) {\n", " var newScript = document.createElement(\"script\");\n", " var attrs = [];\n", " var nodemap = oldScript.attributes;\n", " for (var j in nodemap) {\n", " if (nodemap.hasOwnProperty(j)) {\n", " attrs.push(nodemap[j])\n", " }\n", " }\n", " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", " oldScript.parentNode.replaceChild(newScript, oldScript);\n", " });\n", " if (JS_MIME_TYPE in output.data) {\n", " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", " }\n", " output_area._hv_plot_id = id;\n", " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", " window.PyViz.plot_index[id] = Bokeh.index[id];\n", " } else {\n", " window.PyViz.plot_index[id] = null;\n", " }\n", " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", " var bk_div = document.createElement(\"div\");\n", " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", " var script_attrs = bk_div.children[0].attributes;\n", " for (var i = 0; i < script_attrs.length; i++) {\n", " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", " }\n", " // store reference to server id on output_area\n", " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", " }\n", "}\n", "\n", "/**\n", " * Handle when an output is cleared or removed\n", " */\n", "function handle_clear_output(event, handle) {\n", " var id = handle.cell.output_area._hv_plot_id;\n", " var server_id = handle.cell.output_area._bokeh_server_id;\n", " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", " if (server_id !== null) {\n", " comm.send({event_type: 'server_delete', 'id': server_id});\n", " return;\n", " } else if (comm !== null) {\n", " comm.send({event_type: 'delete', 'id': id});\n", " }\n", " delete PyViz.plot_index[id];\n", " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", " var doc = window.Bokeh.index[id].model.document\n", " doc.clear();\n", " const i = window.Bokeh.documents.indexOf(doc);\n", " if (i > -1) {\n", " window.Bokeh.documents.splice(i, 1);\n", " }\n", " }\n", "}\n", "\n", "/**\n", " * Handle kernel restart event\n", " */\n", "function handle_kernel_cleanup(event, handle) {\n", " delete PyViz.comms[\"hv-extension-comm\"];\n", " window.PyViz.plot_index = {}\n", "}\n", "\n", "/**\n", " * Handle update_display_data messages\n", " */\n", "function handle_update_output(event, handle) {\n", " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", " handle_add_output(event, handle)\n", "}\n", "\n", "function register_renderer(events, OutputArea) {\n", " function append_mime(data, metadata, element) {\n", " // create a DOM node to render to\n", " var toinsert = this.create_output_subarea(\n", " metadata,\n", " CLASS_NAME,\n", " EXEC_MIME_TYPE\n", " );\n", " this.keyboard_manager.register_events(toinsert);\n", " // Render to node\n", " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", " render(props, toinsert[0]);\n", " element.append(toinsert);\n", " return toinsert\n", " }\n", "\n", " events.on('output_added.OutputArea', handle_add_output);\n", " events.on('output_updated.OutputArea', handle_update_output);\n", " events.on('clear_output.CodeCell', handle_clear_output);\n", " events.on('delete.Cell', handle_clear_output);\n", " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", "\n", " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", " safe: true,\n", " index: 0\n", " });\n", "}\n", "\n", "if (window.Jupyter !== undefined) {\n", " try {\n", " var events = require('base/js/events');\n", " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", " register_renderer(events, OutputArea);\n", " }\n", " } catch(err) {\n", " }\n", "}\n" ], "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/vnd.holoviews_exec.v0+json": "", "text/html": [ "
\n", "
\n", "
\n", "" ] }, "metadata": { "application/vnd.holoviews_exec.v0+json": { "id": "p1002" } }, "output_type": "display_data" }, { "data": { "text/html": [ "\n", "
\n", "\n", "\n", "\n", "\n", "\n", " \n", " \n", "\n", "\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import warnings\n", "\n", "import geoviews as gv\n", "import geoviews.feature as gf\n", "import holoviews as hv\n", "\n", "with warnings.catch_warnings(action='ignore', category=FutureWarning):\n", " gv.extension('matplotlib')" ] }, { "cell_type": "markdown", "id": "ea61417d-df07-472e-bfcd-6ee23f74eaa4", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 3. Define a function for plotting earthquakes on a map using GeoViews.\n", "\n", "Next, we will write a function to plot each earthquake as a point on the world map. Since our dataset has geometries, we can use that information to plot them and then color each point by the earthquake magnitude. Note that, since earthquakes are measured on a logarithmic scale, some magnitudes are negative:" ] }, { "cell_type": "code", "execution_count": 31, "id": "7fba7d91-0192-4a92-ac1c-bbb7f146497f", "metadata": {}, "outputs": [], "source": [ "import calendar\n", "\n", "def plot_earthquakes(data, month_num):\n", " points = gv.Points(\n", " data.query(f'month == {month_num}'),\n", " kdims=['longitude', 'latitude'], # key dimensions (for coordinates in this case)\n", " vdims=['mag'] # value dimensions (for modifying the plot in this case)\n", " ).redim.range(mag=(-2, 10), latitude=(-90, 90))\n", "\n", " # create an overlay by combining Cartopy features and the points with *\n", " overlay = gf.land * gf.coastline * gf.borders * points\n", "\n", " return overlay.opts(\n", " gv.opts.Points(color='mag', cmap='fire_r', colorbar=True, alpha=0.75),\n", " gv.opts.Overlay(\n", " global_extent=False, title=calendar.month_name[month_num], fontscale=2\n", " )\n", " )" ] }, { "cell_type": "markdown", "id": "18b6add3-6a04-4737-a69f-d08d87df7ecc", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "Our function returns an `Overlay` of earthquakes (represented as `Points`) on a map of the world. Under the hood GeoViews is using [Cartopy](https://scitools.org.uk/cartopy/docs/latest/) to create the map:" ] }, { "cell_type": "code", "execution_count": 32, "id": "fc033808-e8bc-4d8d-8509-f095b59f31e6", "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ ":Overlay\n", " .Land.I :Feature [Longitude,Latitude]\n", " .Coastline.I :Feature [Longitude,Latitude]\n", " .Borders.I :Feature [Longitude,Latitude]\n", " .Points.I :Points [longitude,latitude] (mag)" ] }, "execution_count": 32, "metadata": { "application/vnd.holoviews_exec.v0+json": {} }, "output_type": "execute_result" } ], "source": [ "plot_earthquakes(earthquakes, 1).opts(\n", " fig_inches=(6, 3), aspect=2, fig_size=250, fig_bounds=(0.07, 0.05, 0.87, 0.95)\n", ")" ] }, { "cell_type": "markdown", "id": "7b2f7555-1c88-4f66-8528-84f92cd2c6b6", "metadata": {}, "source": [ "*Tip: One thing that makes working with geospatial data difficult is handling [projections](https://en.wikipedia.org/wiki/Map_projection). When working with datasets that use different projections, GeoViews can help align them – check out their tutorial [here](https://geoviews.org/user_guide/Projections.html).*" ] }, { "cell_type": "markdown", "id": "97720cc4-de60-4277-9837-f76938a4fe9e", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 4. Create a mapping of frames to plots using HoloViews.\n", "We will create a `HoloMap` of the frames to include in our animation. This maps the frame to the plot that should be rendered at that frame:" ] }, { "cell_type": "code", "execution_count": 33, "id": "cd3ee499-954b-4222-a0ec-668d03b62bae", "metadata": {}, "outputs": [], "source": [ "frames = {\n", " month_num: plot_earthquakes(earthquakes, month_num)\n", " for month_num in range(1, 13)\n", "}\n", "holomap = hv.HoloMap(frames)" ] }, { "cell_type": "markdown", "id": "17d8e73f-f8c4-49eb-a7b9-2aa97f378f7f", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "#### 5. Animate the plot.\n", "Now, we will output our `HoloMap` as a GIF animation, which may take a while to run:" ] }, { "cell_type": "code", "execution_count": 34, "id": "a79130cb-961c-49c9-af68-bf1bc57142a8", "metadata": { "tags": [] }, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ ":HoloMap [Default]\n", " :Overlay\n", " .Land.I :Feature [Longitude,Latitude]\n", " .Coastline.I :Feature [Longitude,Latitude]\n", " .Borders.I :Feature [Longitude,Latitude]\n", " .Points.I :Points [longitude,latitude] (mag)" ] }, "metadata": { "application/vnd.holoviews_exec.v0+json": {} }, "output_type": "display_data" } ], "source": [ "hv.output(\n", " holomap.opts(\n", " fig_inches=(6, 3), aspect=2, fig_size=250,\n", " fig_bounds=(0.07, 0.05, 0.87, 0.95)\n", " ), holomap='gif', fps=5\n", ")" ] }, { "cell_type": "markdown", "id": "d7667ec3-0578-41a1-8b77-b5af380892ea", "metadata": { "slideshow": { "slide_type": "subslide" }, "tags": [] }, "source": [ "To save the animation to a file, run the following code:\n", "\n", "```python\n", "hv.save(\n", " holomap.opts(\n", " fig_inches=(6, 3), aspect=2, fig_size=250,\n", " fig_bounds=(0.07, 0.05, 0.87, 0.95)\n", " ), 'earthquakes.gif', fps=5\n", ")\n", "```" ] }, { "cell_type": "markdown", "id": "653d244f-c191-40ab-a4de-7f775f990828", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "### [Exercise 2.2](./workbook.ipynb#Exercise-2.2)\n", "\n", "##### Modify the earthquake animation to show earthquakes per day in April 2020." ] }, { "cell_type": "code", "execution_count": 35, "id": "69be97df-3724-4b9b-a70e-4fa02d06be24", "metadata": {}, "outputs": [], "source": [ "# Complete this exercise in the `workbook.ipynb` file" ] }, { "cell_type": "code", "execution_count": 36, "id": "8128bc8f-b7c3-4f4f-b094-066669d50fd8", "metadata": {}, "outputs": [], "source": [ "# Click on `Exercise 2.2` above to open the `workbook.ipynb` file" ] }, { "cell_type": "markdown", "id": "16527d2f-0c14-4783-99c8-c897ec0b69ab", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "## Additional resources\n", "\n", "- `matplotlib.animation` [API overview](https://matplotlib.org/stable/api/animation_api.html)\n", "- `FuncAnimation` [documentation](https://matplotlib.org/stable/api/_as_gen/matplotlib.animation.FuncAnimation.html)\n", "- Matplotlib animation [examples](https://matplotlib.org/stable/api/animation_api.html#examples) \n", "- Matplotlib's list of [3rd-party animation libraries](https://matplotlib.org/stable/thirdpartypackages/index.html#animations)\n", "- Using HoloViews with the [Matplotlib backend](http://holoviews.org/user_guide/Plotting_with_Matplotlib.html)" ] }, { "cell_type": "markdown", "id": "3f393fe3-2f97-4117-b8b1-7ff5f242755e", "metadata": { "slideshow": { "slide_type": "slide" }, "tags": [] }, "source": [ "## Up Next: [Building Interactive Visualizations for Data Exploration](./3-interactivity.ipynb)" ] } ], "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.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }