{ "cells": [ { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "# Interactive plots with Ipyleaflet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview:\n", "1. Interactively display images from remote map servers with ipyleaflet\n", "2. Overlay gridded data on the interactive map " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " Older Enivronment Note that we are using an older Python 3 environment in this notebook! The ipyleaflet package is currently incompatible with the version of Jupyterlab that we have been using this semester.\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Prerequisites\n", "\n", "| Concepts | Importance | Notes |\n", "| --- | --- | --- |\n", "| Contextily | |\n", "| Xarray | |\n", "\n", "* **Time to learn**: 20 minutes\n", "***" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Imports" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from datetime import datetime, timedelta\n", "from ipyleaflet import Map, TileLayer, basemaps, basemap_to_tiles\n", "from ipyleaflet.velocity import Velocity\n", "import xarray as xr" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interactively display images from remote map servers with ipyleaflet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "While we've made some nice looking maps so far in the course, they are all *static* images ... i.e., they can only be viewed ... not interacted with. Most of us have browsed interactive map-based websites, such as Google Maps, where we can use our mouse or touch-enabled screen to move around, zoom in or out, or otherwise intereact with the plot. The **Javascript** programming language is what makes this interactivity possible. Here, we will use a Python package, [ipyleaflet](https://ipyleaflet.readthedocs.io/en/latest/), as a \"bridge\" to Javascript." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " Note: The \"leaflet\" in ipyleaflet refers to the Leaflet open-source Javascript library.\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Use a satellite composite as the base image.\n", "Now, specify an image tile server and product, using the same sources as those provided by Contextily. Center the map and specify a zoom level." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create time strings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll need to select a date and time, and then pass in date/time objects or strings to various functions below; their format will be function-specific." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "timeObj = datetime(2024,4,25,0)\n", "timeStr1 = timeObj.strftime(\"%Y-%m-%d\")\n", "timeStr2 = timeObj.strftime(\"%Y%m%d_%H%M\")" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'20240425_0000'" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "timeStr2" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "ca871b4025e34dd0aa6d46b9b14e9902", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Map(center=[42.204793, -74.121558], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title'…" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m = Map(\n", " basemap=basemap_to_tiles(basemaps.NASAGIBS.ModisTerraTrueColorCR, timeStr1),\n", " center=(42.204793, -74.121558),\n", " zoom=4\n", ")\n", "\n", "m" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Add a background cartographic layer.\n", "We'll use ipyleaflet's `add_layer` method. We simply add another remote tile server. The default `opacity` is 1.0, so reduce that a bit so we still see the satellite layer." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "basemapLayer = basemap_to_tiles(basemaps.CartoDB.Positron,opacity=0.5)\n", "m.add_layer(basemapLayer)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interact with the dynamic map" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Zoom in and out with the +/- buttons to see both layers dynamically update." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overlay gridded data on the interactive map" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will display an animated wind velocity flow field, a la https://earth.nullschool.net or https://www.windy.com/. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " Note: ipyleaflet can read in Xarray DataArray objects so we will exploit that here.\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Read in a dataset based on the 0.5-degree resolution GFS on a given date and time." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "ds = xr.open_dataset('https://thredds.ucar.edu/thredds/dodsC/grib/NCEP/GFS/Global_0p5deg_ana/GFS_Global_0p5deg_ana_'+ timeStr2 +'.grib2')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Examine the dataset" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:                                                          (lat: 361,\n",
       "                                                                      lon: 720,\n",
       "                                                                      time: 1,\n",
       "                                                                      altitude_above_msl: 3,\n",
       "                                                                      sigma_layer: 4,\n",
       "                                                                      pressure_difference_layer: 1,\n",
       "                                                                      ...\n",
       "                                                                      sigma: 1,\n",
       "                                                                      pressure_difference_layer1: 1,\n",
       "                                                                      hybrid: 1,\n",
       "                                                                      sigma_layer_bounds_1: 2,\n",
       "                                                                      pressure_difference_layer_bounds_1: 2,\n",
       "                                                                      pressure_difference_layer1_bounds_1: 2)\n",
       "Coordinates: (12/15)\n",
       "  * lat                                                              (lat) float32 ...\n",
       "  * lon                                                              (lon) float32 ...\n",
       "    reftime                                                          datetime64[ns] ...\n",
       "  * time                                                             (time) datetime64[ns] ...\n",
       "  * altitude_above_msl                                               (altitude_above_msl) float32 ...\n",
       "  * sigma_layer                                                      (sigma_layer) float32 ...\n",
       "    ...                                                               ...\n",
       "  * height_above_ground                                              (height_above_ground) float32 ...\n",
       "  * potential_vorticity_surface                                      (potential_vorticity_surface) float32 ...\n",
       "  * height_above_ground1                                             (height_above_ground1) float32 ...\n",
       "  * sigma                                                            (sigma) float32 ...\n",
       "  * pressure_difference_layer1                                       (pressure_difference_layer1) float32 ...\n",
       "  * hybrid                                                           (hybrid) float32 ...\n",
       "Dimensions without coordinates: sigma_layer_bounds_1,\n",
       "                                pressure_difference_layer_bounds_1,\n",
       "                                pressure_difference_layer1_bounds_1\n",
       "Data variables: (12/83)\n",
       "    LatLon_Projection                                                int32 ...\n",
       "    sigma_layer_bounds                                               (sigma_layer, sigma_layer_bounds_1) float32 ...\n",
       "    pressure_difference_layer_bounds                                 (pressure_difference_layer, pressure_difference_layer_bounds_1) float32 ...\n",
       "    pressure_difference_layer1_bounds                                (pressure_difference_layer1, pressure_difference_layer1_bounds_1) float32 ...\n",
       "    Absolute_vorticity_isobaric                                      (time, isobaric, lat, lon) float32 ...\n",
       "    Cloud_mixing_ratio_hybrid                                        (time, hybrid, lat, lon) float32 ...\n",
       "    ...                                                               ...\n",
       "    v-component_of_wind_potential_vorticity_surface                  (time, potential_vorticity_surface, lat, lon) float32 ...\n",
       "    v-component_of_wind_altitude_above_msl                           (time, altitude_above_msl, lat, lon) float32 ...\n",
       "    v-component_of_wind_maximum_wind                                 (time, lat, lon) float32 ...\n",
       "    v-component_of_wind_height_above_ground                          (time, height_above_ground1, lat, lon) float32 ...\n",
       "    v-component_of_wind_tropopause                                   (time, lat, lon) float32 ...\n",
       "    v-component_of_wind_sigma                                        (time, sigma, lat, lon) float32 ...\n",
       "Attributes:\n",
       "    Originating_or_generating_Center:                                        ...\n",
       "    Originating_or_generating_Subcenter:                                     ...\n",
       "    GRIB_table_version:                                                      ...\n",
       "    Type_of_generating_process:                                              ...\n",
       "    Analysis_or_forecast_generating_process_identifier_defined_by_originating...\n",
       "    file_format:                                                             ...\n",
       "    Conventions:                                                             ...\n",
       "    history:                                                                 ...\n",
       "    featureType:                                                             ...\n",
       "    _CoordSysBuilder:                                                        ...
" ], "text/plain": [ "\n", "Dimensions: (lat: 361,\n", " lon: 720,\n", " time: 1,\n", " altitude_above_msl: 3,\n", " sigma_layer: 4,\n", " pressure_difference_layer: 1,\n", " ...\n", " sigma: 1,\n", " pressure_difference_layer1: 1,\n", " hybrid: 1,\n", " sigma_layer_bounds_1: 2,\n", " pressure_difference_layer_bounds_1: 2,\n", " pressure_difference_layer1_bounds_1: 2)\n", "Coordinates: (12/15)\n", " * lat (lat) float32 ...\n", " * lon (lon) float32 ...\n", " reftime datetime64[ns] ...\n", " * time (time) datetime64[ns] ...\n", " * altitude_above_msl (altitude_above_msl) float32 ...\n", " * sigma_layer (sigma_layer) float32 ...\n", " ... ...\n", " * height_above_ground (height_above_ground) float32 ...\n", " * potential_vorticity_surface (potential_vorticity_surface) float32 ...\n", " * height_above_ground1 (height_above_ground1) float32 ...\n", " * sigma (sigma) float32 ...\n", " * pressure_difference_layer1 (pressure_difference_layer1) float32 ...\n", " * hybrid (hybrid) float32 ...\n", "Dimensions without coordinates: sigma_layer_bounds_1,\n", " pressure_difference_layer_bounds_1,\n", " pressure_difference_layer1_bounds_1\n", "Data variables: (12/83)\n", " LatLon_Projection int32 ...\n", " sigma_layer_bounds (sigma_layer, sigma_layer_bounds_1) float32 ...\n", " pressure_difference_layer_bounds (pressure_difference_layer, pressure_difference_layer_bounds_1) float32 ...\n", " pressure_difference_layer1_bounds (pressure_difference_layer1, pressure_difference_layer1_bounds_1) float32 ...\n", " Absolute_vorticity_isobaric (time, isobaric, lat, lon) float32 ...\n", " Cloud_mixing_ratio_hybrid (time, hybrid, lat, lon) float32 ...\n", " ... ...\n", " v-component_of_wind_potential_vorticity_surface (time, potential_vorticity_surface, lat, lon) float32 ...\n", " v-component_of_wind_altitude_above_msl (time, altitude_above_msl, lat, lon) float32 ...\n", " v-component_of_wind_maximum_wind (time, lat, lon) float32 ...\n", " v-component_of_wind_height_above_ground (time, height_above_ground1, lat, lon) float32 ...\n", " v-component_of_wind_tropopause (time, lat, lon) float32 ...\n", " v-component_of_wind_sigma (time, sigma, lat, lon) float32 ...\n", "Attributes:\n", " Originating_or_generating_Center: ...\n", " Originating_or_generating_Subcenter: ...\n", " GRIB_table_version: ...\n", " Type_of_generating_process: ...\n", " Analysis_or_forecast_generating_process_identifier_defined_by_originating...\n", " file_format: ...\n", " Conventions: ...\n", " history: ...\n", " featureType: ...\n", " _CoordSysBuilder: ..." ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Specify an analysis time, and then create `DataArray` objects that are subset for the specified time and the 20m AGL level (this is the lowest level in the dataset)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "u = ds['u-component_of_wind_height_above_ground'].sel(time=timeObj,height_above_ground1=20)\n", "v = ds['v-component_of_wind_height_above_ground'].sel(time=timeObj,height_above_ground1=20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create a Dataset that contains just the wind components" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "ipyleaflet's `Velocity` method produces an animated streamline effect. It accepts an Xarray dataset that contains just two arrays ... specifically, the u- and v- components of the wind vector we have chosen for visualization.\n", "\n", "(Perhaps there is a more efficient way of doing this, but the following cell will create a new Xarray Dataset that consists of just two arrays ... u and v.)" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "
<xarray.Dataset>\n",
       "Dimensions:               (lat: 361, lon: 720)\n",
       "Coordinates:\n",
       "  * lat                   (lat) float32 90.0 89.5 89.0 ... -89.0 -89.5 -90.0\n",
       "  * lon                   (lon) float32 0.0 0.5 1.0 1.5 ... 358.5 359.0 359.5\n",
       "    reftime               datetime64[ns] ...\n",
       "    time                  datetime64[ns] 2024-04-25\n",
       "    height_above_ground1  float32 20.0\n",
       "Data variables:\n",
       "    u                     (lat, lon) float32 ...\n",
       "    v                     (lat, lon) float32 ...
" ], "text/plain": [ "\n", "Dimensions: (lat: 361, lon: 720)\n", "Coordinates:\n", " * lat (lat) float32 90.0 89.5 89.0 ... -89.0 -89.5 -90.0\n", " * lon (lon) float32 0.0 0.5 1.0 1.5 ... 358.5 359.0 359.5\n", " reftime datetime64[ns] ...\n", " time datetime64[ns] 2024-04-25\n", " height_above_ground1 float32 20.0\n", "Data variables:\n", " u (lat, lon) float32 ...\n", " v (lat, lon) float32 ..." ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# First create the Dataset out of the 'u' Dataarray\n", "dsWind = u.to_dataset()\n", "\n", "# Append v to the Dataset\n", "dsWind['v'] = v\n", "\n", "# Rename the u DataArray to 'u'\n", "name_dict = {'u-component_of_wind_height_above_ground':'u'}\n", "dsWind = dsWind.rename_vars(name_dict)\n", "\n", "# Verify that the Dataset looks ok\n", "dsWind" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Create the animation" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Center it, specify a zoom level, and select a background map; create and add a velocity layer from the Dataset that contains u and v." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "center = (40.0, -70.0)\n", "zoom = 6\n", "m = Map(center=center, zoom=zoom, interpolation='nearest', basemap=basemaps.CartoDB.DarkMatter)\n", "\n", "\n", "display_options = {\n", " 'velocityType': 'Global Wind',\n", " 'displayPosition': 'bottomleft',\n", " 'displayEmptyString': 'No wind data'\n", "}\n", "wind = Velocity(data=dsWind,\n", " zonal_speed='u',\n", " meridional_speed='v',\n", " latitude_dimension='lat',\n", " longitude_dimension='lon',\n", " velocity_scale=0.01,\n", " max_velocity=120,\n", " display_options=display_options)\n", "m.add_layer(wind)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Display the map." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "370fcb83d24042c9a2076c4aec675e67", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Map(center=[40.0, -70.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_ou…" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "m" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you move your mouse/touchpad, you will see direction and speed at the nearest gridpoint displayed at the bottom left of the frame." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " Things to try:
\n", "
  1. Create a velocity map using gridded data from another source ... such as RDA or another datasource from the Unidata THREDDS Server.
  2. \n", "
  3. Make this notebook dynamic in terms of date so it will always provide a recent (e.g. one day ago) visualization.
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Summary\n", "* Ipyleaflet provides Javascript-enabled interactivity in a Jupyter notebook.\n", "* Ipyleaflet's `Velocity` function creates a flow-vector field that can be layered onto a basemap.\n", "* The `Velocity` function accepts an Xarray dataset that contains the u- and v- wind components.\n", "\n", "### What's Next?\n", "We'll continue to explore interactive visualizations, next with the **Holoviz** suite of packages." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## References:\n", "1. [ipyleaflet](https://ipyleaflet.readthedocs.io/en/latest/)\n", "1. [Leaflet](https://leafletjs.com)\n", "1. [ipyleaflet Map](https://ipyleaflet.readthedocs.io/en/latest/api_reference/map.html)\n", "1. [ipyleaflet Basemaps](https://ipyleaflet.readthedocs.io/en/latest/api_reference/basemaps.html)\n", "1. [ipyleaflet tile_layer](https://ipyleaflet.readthedocs.io/en/latest/api_reference/tile_layer.html)\n", "1. [Remote basemap providers](https://contextily.readthedocs.io/en/latest/providers_deepdive.html)\n", "1. [ipyleaflet Velocity](https://ipyleaflet.readthedocs.io/en/latest/api_reference/velocity.html)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 Jan. 2024 Environment", "language": "python", "name": "jan24" }, "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.11.7" } }, "nbformat": 4, "nbformat_minor": 4 }