diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe5f4559..4fa4c46d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,6 +47,5 @@ repos: additional_dependencies: [black] - id: nbqa-pyupgrade additional_dependencies: [pyupgrade] - exclude: foundations/quickstart.ipynb - id: nbqa-isort additional_dependencies: [isort] diff --git a/README.md b/README.md index 128d7c3b..8831fcb0 100644 --- a/README.md +++ b/README.md @@ -9,25 +9,28 @@ [![Binder](https://binder.projectpythia.org/badge_logo.svg)](https://binder.projectpythia.org/v2/gh/ProjectPythia/unstructured-grid-viz-cookbook.git/main?labpath=notebooks) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.10403389.svg)](https://doi.org/10.5281/zenodo.10403389) -This Cookbook is a comprehensive showcase of workflows & techniques for visualizing Unstructured Grids using [UXarray](https://uxarray.readthedocs.io/). +This Cookbook is a comprehensive showcase of workflows & techniques for visualizing Unstructured Grids using [UXarray](https://uxarray.readthedocs.io/), +also providing foundational information on unstructured grids. ## Motivation -The ability to natively visualize unstructured grids is a much-needed ability within the Scientific Python Ecosystem, +The ability to natively visualize unstructured grids is much needed within the Scientific Python Ecosystem, which poses multiple challenges and needs to: -- Not regrid the source unstructured grid to structure +- Not regrid the source unstructured grid to structured grid - Take advantage of grid information, such as connectivity variables - Limit the amount of pre-processing needed to prepare the data for Python visualization tools -UXarray enables such visualization methods that operate directly on unstructured grid data, -providing Xarray-styled functionality to better read in -and use unstructured grid datasets that follow standard conventions. -UXarray supports a variety of unstructured grid formats including UGRID, MPAS, SCRIP, and Exodus, -and is extendable for other formats. +UXarray enables such visualization methods that operate directly on unstructured grid data, providing +Xarray-styled functionality to better read in and use unstructured grid datasets that follow standard +conventions. -This cookbook covers an introduction to unstructured grids and UXarray, -provides an overview of the visualization methods and libraries, and showcases several UXarray visualization functions. +UXarray supports a variety of unstructured grid formats and file types including UGRID, MPAS, ICON, CAM-SE, +SCRIP, Exodus, ESMF, GEOS, and FESOM2, and is extensible for other formats. + +This cookbook covers an introduction to unstructured grids and UXarray from a visualization standpoint, +providing foundational information about unstructured grids, visualization methods and libraries, and +introducing UXarray, and showcasing several UXarray visualization functions and workflows. ## Authors @@ -47,19 +50,32 @@ provides an overview of the visualization methods and libraries, and showcases s ## Structure -This cookbook is split up into a few chapters that provide a detailed overview of how to use UXarray to work with and visualize unstructured grid datasets: +This cookbook is split up into several chapters to communicate the content efffectively with different +levels of readers: + +**1. Foundations** + +Here, we cover overview of the foundational topics necessary to understand the content in this cookbook, +e.g. what unstructured grids are and how they are different than structured grids, what plotting libraries +and visualization techniques exist that can be helpful for unstructured grid visualization, and we briefly +mention how UXarray is related to these. -**1. Introduction to UXarray & Unstructured Grids** +**2. Introduction to UXarray** -Here we cover what unstructured grids are and how they are different than structured grids as well as whay UXarray could play a significant role in unstructured grid visualization. +In this chapter, we provide an overview of UXarray: An Xarray-extension for unstructured grid-formatted +climate and global weather data analysis and visualization. -**2. Methods & Libraries for Unstructured Grid Visualization** +**3. Plotting with UXarray** -In this chapter, we briefly introduce plotting libraries and their specific technologies as well as rendering techniques that could be used for unstructured grid plotting and are used as part of UXarray. +We provide an overview of the UXarray plotting API along with several visualization functionality, and cases +and examples that can be realized using such UXarray functionality; Grid visualization, Data visualization, +Geographic projections and features, to name a few. Also in this section, customization and interactivaity +with UXarray plotting and considerations with high-resolution plotting are also provided. -**3. UXarray Visualization** +**4. Visualization Recipies** -Several visualization cases and examples that can be realized using UXarray are provided in this chapter; grid topology plots, polygons, points, to name a few. Also in this section, the usage of UXarray plotting API and a discussion of visualization at scale are also provided. +In this last chapter, we offer to the interested readers a set of focused workflows that can be realized +with UXarray, including visualizations of MPAS and E3SM model output. ## Running the Notebooks diff --git a/_config.yml b/_config.yml index bef5b978..db383ccc 100644 --- a/_config.yml +++ b/_config.yml @@ -5,7 +5,7 @@ title: Unstructured Grids Visualization Cookbook description: Comprehensive showcase of workflows and techniques for visualizing Unstructured Grids using UXarray author: the Project Pythia Community logo: notebooks/images/logos/pythia_logo-white-rtext.svg -copyright: "2024" +copyright: "2025" execute: # To execute notebooks via a Binder instead, replace 'cache' with 'binder' diff --git a/_static/custom.css b/_static/custom.css index b0e83df1..59f43131 100644 --- a/_static/custom.css +++ b/_static/custom.css @@ -1,6 +1,6 @@ .bd-main .bd-content .bd-article-container { - max-width: 100%; /* default is 60em */ + max-width: 100%; /* default is 60em */ } .bd-page-width { - max-width: 100%; /* default is 88rem */ + max-width: 100%; /* default is 88rem */ } diff --git a/_static/images/grids.png b/_static/images/grids.png new file mode 100644 index 00000000..d5bfc8e7 Binary files /dev/null and b/_static/images/grids.png differ diff --git a/_static/images/uxarray-design.png b/_static/images/uxarray-design.png new file mode 100644 index 00000000..1d9d386e Binary files /dev/null and b/_static/images/uxarray-design.png differ diff --git a/_templates/footer-extra.html b/_templates/footer-extra.html index 27cb915d..2b794d89 100644 --- a/_templates/footer-extra.html +++ b/_templates/footer-extra.html @@ -1,27 +1,27 @@ diff --git a/_toc.yml b/_toc.yml index bac8f544..9b277ee2 100644 --- a/_toc.yml +++ b/_toc.yml @@ -4,23 +4,37 @@ parts: - caption: Preamble chapters: - file: notebooks/how-to-cite - - caption: Introduction to UXarray & Unstructured Grids + + - caption: Foundations + chapters: + - file: notebooks/01-foundations/unstructured-grids + - file: notebooks/01-foundations/plotting-libs + - file: notebooks/01-foundations/rendering-techniques + + - caption: Introduction to UXarray chapters: - - file: notebooks/01-intro/01-unstructured-grid-overview - - file: notebooks/01-intro/02-data-structures - - file: notebooks/01-intro/03-data-mapping + - file: notebooks/02-intro-to-uxarray/overview + - file: notebooks/02-intro-to-uxarray/grid + - file: notebooks/02-intro-to-uxarray/uxds-uxda + - file: notebooks/02-intro-to-uxarray/selection - - caption: Methods & Libraries for Unstructured Grid Visualization + - caption: Plotting with UXarray chapters: - - file: notebooks/02-methods/01-plotting-libraries - - file: notebooks/02-methods/02-rendering-techniques + - file: notebooks/03-plotting-with-uxarray/grid-viz + - file: notebooks/03-plotting-with-uxarray/data-viz + - file: notebooks/03-plotting-with-uxarray/geo + - file: notebooks/03-plotting-with-uxarray/customization + - file: notebooks/03-plotting-with-uxarray/high-res + - file: notebooks/03-plotting-with-uxarray/compare-xarray - - caption: UXarray Visualization + - caption: Visualization Recipes chapters: - - file: notebooks/03-uxarray-vis/01-plot-api - - file: notebooks/03-uxarray-vis/002-xarray-to-uxarray - - file: notebooks/03-uxarray-vis/02-grid-topology - - file: notebooks/03-uxarray-vis/03-polygons - - file: notebooks/03-uxarray-vis/04-points - - file: notebooks/03-uxarray-vis/07-animations - - file: notebooks/03-uxarray-vis/06-performance + - file: notebooks/04-recipes/mpas-atmo + - file: notebooks/04-recipes/mpas-ocean + # - file: notebooks/04-recipes/mpas-regional + - file: notebooks/04-recipes/e3sm +# - caption: Compatibility with Visualization Packages +# chapters: +# - file: notebooks/05-viz-packages/matplotlib +# - file: notebooks/05-viz-packages/datashader +# - file: notebooks/05-viz-packages/lonboard diff --git a/meshfiles/hex.data.nc b/meshfiles/hex.data.nc new file mode 100644 index 00000000..5fcd36bb Binary files /dev/null and b/meshfiles/hex.data.nc differ diff --git a/meshfiles/hex.grid.nc b/meshfiles/hex.grid.nc new file mode 100644 index 00000000..aa38e3d4 Binary files /dev/null and b/meshfiles/hex.grid.nc differ diff --git a/meshfiles/hex.node.data.nc b/meshfiles/hex.node.data.nc new file mode 100644 index 00000000..fa13a90d Binary files /dev/null and b/meshfiles/hex.node.data.nc differ diff --git a/meshfiles/ne30pg2.data.nc b/meshfiles/ne30pg2.data.nc new file mode 100644 index 00000000..edc7db42 Binary files /dev/null and b/meshfiles/ne30pg2.data.nc differ diff --git a/meshfiles/ne30pg2.grid.nc b/meshfiles/ne30pg2.grid.nc new file mode 100644 index 00000000..ae86055f Binary files /dev/null and b/meshfiles/ne30pg2.grid.nc differ diff --git a/meshfiles/x1.655362.data.nc b/meshfiles/x1.655362.data.nc new file mode 100644 index 00000000..87f02794 Binary files /dev/null and b/meshfiles/x1.655362.data.nc differ diff --git a/meshfiles/x1.655362.grid.nc b/meshfiles/x1.655362.grid.nc new file mode 100644 index 00000000..148a63ab Binary files /dev/null and b/meshfiles/x1.655362.grid.nc differ diff --git a/notebooks/01-foundations/plotting-libs.ipynb b/notebooks/01-foundations/plotting-libs.ipynb new file mode 100644 index 00000000..23bdb08e --- /dev/null +++ b/notebooks/01-foundations/plotting-libs.ipynb @@ -0,0 +1,371 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a107dfa27c1b5b7b", + "metadata": { + "panel-layout": { + "height": 423.611, + "visible": true, + "width": 100 + } + }, + "source": [ + "\"UXarray\n", + "\n", + "# Plotting Libraries\n", + "\n", + "### In this section, you'll learn:\n", + "\n", + "* What general purpose visualization packages are available in the Scientific Python Ecosystem\n", + "* How functionality of these packages can be useful for unstructured grids visualization\n", + "\n", + "### Related Documentation\n", + "\n", + "* No UXarray documentation is referenced here since this section covers the content from a general point of view.\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| Python | Necessary | This notebook does not contain any Python code, but understanding those libraries comprehensively would require Python programming |\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "c6502995-401e-4f39-b789-1c39e93b02e8", + "metadata": { + "panel-layout": { + "height": 206.753, + "visible": true, + "width": 100 + } + }, + "source": [ + "In this section, we will introduce two visualization libraries/technologies and their features and functionality that can be useful for unstructured grids visualization before diving deep into plotting unstructured grids with UXarray in the next chapter." + ] + }, + { + "cell_type": "markdown", + "id": "aba250fa-bfe5-4e65-8f76-93bbc9466d67", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 50.816, + "visible": true, + "width": 100 + } + }, + "source": [ + "## HoloViz" + ] + }, + { + "cell_type": "markdown", + "id": "2f0181f8-2859-4a13-853e-9f4c89b7ad8d", + "metadata": { + "panel-layout": { + "height": 491.51, + "visible": true, + "width": 100 + } + }, + "source": [ + "[HoloViz](https://holoviz.org/) is a stack of tools (such as Holoviews, Datashader, Geoviews, SpatialPandas, hvPlot etc.) that provide high-level functionality to visualize even the very large datasets efficiently in Python. HoloViz packages are well-suited for unstructured grid visualization because:\n", + "\n", + "1. They provide rendering functionality for both vector geometries and rasterization, which will be detailed in the [next section](rendering-techniques). Such functionality is much needed for unstructured grid topology and data visualization purposes.\n", + "2. Unlike Matplotlib, they support using the connectivity information that comes from the unstructured grids\n", + "3. They are designed to be scalable for even the largest datasets that'd be generated as result of kilometer-scale analyses" + ] + }, + { + "cell_type": "markdown", + "id": "1e2b2a13-9bc4-43fc-a3e0-0f77cfbc2efc", + "metadata": { + "panel-layout": { + "height": 102.535, + "visible": true, + "width": 100 + } + }, + "source": [ + "Let us look at the particular HoloViz packages that can be useful for scalable visualization of unstructured grids." + ] + }, + { + "cell_type": "markdown", + "id": "2768839a-22d2-4543-8856-763bbd737c12", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 217.5, + "visible": true, + "width": 100 + } + }, + "source": [ + "### HoloViews\n", + "\n", + "[Holoviews](https://holoviews.org/about.html) houses several elements (e.g. `Path()`, `Points()`) that enables visualization of grid geometries such as nodes and edges. Similarly, other elements of this package (e.g. `Polygons()`) can be used for various polygon vector visulization purposes." + ] + }, + { + "cell_type": "markdown", + "id": "14578d1d-1582-4155-a845-9287b02937b7", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 44.0799, + "visible": true, + "width": 100 + } + }, + "source": [ + "### Datashader" + ] + }, + { + "cell_type": "markdown", + "id": "8552a1e4-2a52-4c4c-8b1f-2171c45923e8", + "metadata": { + "panel-layout": { + "height": 171.076, + "visible": true, + "width": 100 + } + }, + "source": [ + "[Datashader](https://datashader.org/about.html) is the graphics pipeline system of the HoloViz tool stack for creating meaningful representations of large datasets quickly and flexibly. Datashader's rasterization methods, transfer functions, and other shading operators can be utilized for rasterized polygon plotting." + ] + }, + { + "cell_type": "markdown", + "id": "ee79d625-0694-4449-b560-14dd3b938587", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 44.0799, + "visible": true, + "width": 100 + } + }, + "source": [ + "### GeoViews" + ] + }, + { + "cell_type": "markdown", + "id": "709f3439-e598-41b9-a3bb-e75c9c6297e9", + "metadata": { + "panel-layout": { + "height": 273.889, + "visible": true, + "width": 100 + } + }, + "source": [ + "[GeoViews](https://geoviews.org/index.html) provides features and functionality to visualize geographical, meteorological, and oceanographic datasets and features." + ] + }, + { + "cell_type": "markdown", + "id": "4630b0ef-3fac-4643-83c1-ad26c84d7207", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 44.0799, + "visible": true, + "width": 100 + } + }, + "source": [ + "### Spatialpandas" + ] + }, + { + "cell_type": "markdown", + "id": "e0dbdb54-839c-48b1-9fd4-f4f93305d696", + "metadata": { + "panel-layout": { + "height": 408.056, + "visible": true, + "width": 100 + } + }, + "source": [ + "Spatialpandas is a package that provides Pandas extension arrays for spatial/geometric operations. This package has an element called `GeoDataFrame`, which can be used directly by packages from the HoloViz stack such as hvPlot, Datashader, Holoviews, and Geoviews. Conversions from unstructured grids to `GeoDataFrame` can allow to perform visualizations directly in HoloViz packages." + ] + }, + { + "cell_type": "markdown", + "id": "32fb6a11-21f5-42f3-8a2e-e8262298fc1c", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 50.816, + "visible": true, + "width": 100 + } + }, + "source": [ + "## Matplotlib" + ] + }, + { + "cell_type": "markdown", + "id": "3fa8566f-2a2d-41d1-8c22-388f315ef56e", + "metadata": { + "panel-layout": { + "height": 533.75, + "visible": true, + "width": 100 + } + }, + "source": [ + "Matplotlib is the workhorse of Python visualization needs, for both general and geoscientific purposes. However, when it \n", + "comes to visualizing unstructured grids, Matplotlib's:\n", + "\n", + "1. Functionality is limited such that there is no way to use the connectivity information that comes with the unstructured grid \n", + "2. Scalability especially for kilometer-scale (e.g. individual storm-resolving) resolutions is limited. \n", + "\n", + "Matplotlib can still serve as a visualization backend for unstriuctured grid visualization, especially for the end-user who is familiar with Matplotlib and would like to create publication-quality outputs.\n", + "\n", + "Moreover, just like conversion to `Spatialpandas.GeoDataFrame`, conversions to Matplotlib data structures such as `Collections` can be utilized for unstructrued grid plotting directly with the Matplotlib interface." + ] + }, + { + "cell_type": "markdown", + "id": "81afad50-801f-4e8b-9022-ced33eb0bdd3", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 183.229, + "visible": true, + "width": 100 + } + }, + "source": [ + "### Collections \n", + "Detailed information about Matplotlib's Collections API can be found [here](https://matplotlib.org/stable/api/collections_api.html). Conversions to `LineCollection` and `PolyCollection` can help visualize Grid Geometries and data variables, respectively. " + ] + }, + { + "cell_type": "markdown", + "id": "f8f1b8bc-70f2-4d7c-a2cd-6c7b20f9d10b", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 44.0796, + "visible": true, + "width": 100 + } + }, + "source": [ + "### Cartopy" + ] + }, + { + "cell_type": "markdown", + "id": "a443de93-18ef-40d6-a24b-f7abb6ab38b6", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + }, + "panel-layout": { + "height": 322.378, + "visible": true, + "width": 100 + } + }, + "source": [ + "Cartopy is originally a Python library for cartographic visualizations with Matplotlib; however, they provide a number of features such as `crs`, i.e. Coordinate Reference Systems (a.k.a. projections), that are significant for cartographic visualizations." + ] + }, + { + "cell_type": "markdown", + "id": "29fc6539-2830-480f-81b1-e6e8d99508b0", + "metadata": {}, + "source": [ + "## What is next?\n", + "The next section will provide an overview of the rendering techniques that can be used for visualizing unstructrued grids." + ] + } + ], + "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.6" + }, + "panel-cell-order": [ + "a107dfa27c1b5b7b", + "c6502995-401e-4f39-b789-1c39e93b02e8", + "aba250fa-bfe5-4e65-8f76-93bbc9466d67", + "2f0181f8-2859-4a13-853e-9f4c89b7ad8d", + "5d8acee1-cff1-440c-ba45-156e9c1b3d01", + "1e2b2a13-9bc4-43fc-a3e0-0f77cfbc2efc", + "2768839a-22d2-4543-8856-763bbd737c12", + "a34f9368-1a91-4e55-bf78-962e09a65bca", + "14578d1d-1582-4155-a845-9287b02937b7", + "8552a1e4-2a52-4c4c-8b1f-2171c45923e8", + "ee79d625-0694-4449-b560-14dd3b938587", + "709f3439-e598-41b9-a3bb-e75c9c6297e9", + "4630b0ef-3fac-4643-83c1-ad26c84d7207", + "e0dbdb54-839c-48b1-9fd4-f4f93305d696", + "32fb6a11-21f5-42f3-8a2e-e8262298fc1c", + "3fa8566f-2a2d-41d1-8c22-388f315ef56e", + "81afad50-801f-4e8b-9022-ced33eb0bdd3", + "b5a3ffb6-d046-4830-b1ff-93eb73b74de8", + "f8f1b8bc-70f2-4d7c-a2cd-6c7b20f9d10b", + "a443de93-18ef-40d6-a24b-f7abb6ab38b6" + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/02-methods/02-rendering-techniques.ipynb b/notebooks/01-foundations/rendering-techniques.ipynb similarity index 51% rename from notebooks/02-methods/02-rendering-techniques.ipynb rename to notebooks/01-foundations/rendering-techniques.ipynb index 375e6b36..745018a8 100644 --- a/notebooks/02-methods/02-rendering-techniques.ipynb +++ b/notebooks/01-foundations/rendering-techniques.ipynb @@ -2,12 +2,45 @@ "cells": [ { "cell_type": "markdown", + "id": "60db6be101e99c83", "metadata": {}, "source": [ + "\"UXarray\n", + "\n", "# Rendering Techniques\n", - "---\n", "\n", - "Since Unstructured Grids require significantly more overhead to represent compared to Structured (a.k.a. Regular) grids, the choice of rendering technique plays an important in obtaining high-resolution, accurate, and scalable visualuations. \n", + "### In this section, you'll learn:\n", + "\n", + "* Rendering methods that can be used for unstructured grids visualization. \n", + "\n", + "\n", + "### Related Documentation\n", + "\n", + "* No UXarray documentation is referenced here since this section covers the content from a general point of view.\n", + "\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| Geometry | Necessary | |\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "40b652f0-4a70-4b08-a0db-702b4d5e1545", + "metadata": {}, + "source": [ + "Since Unstructured Grids require significantly more overhead to represent compared to Structured grids, the choice of rendering technique plays an important role in obtaining high-resolution, accurate, and scalable visualizations. \n", "\n", "\n", "This notebook introduces relevant concepts and techniques that will be mentioned and used throughout this Cookbook." @@ -15,6 +48,7 @@ }, { "cell_type": "markdown", + "id": "95a35645-2c16-4506-8ae7-abf5e6f8c7e2", "metadata": { "collapsed": false, "jupyter": { @@ -24,13 +58,13 @@ "source": [ "## Vector (Shape) Geometries\n", "\n", - "The nodes, edges, and faces that make up an Unstructured Grid can each be converted into a geometric shape for visualization. These geometric shapes can often be referred to as vector graphics, since each geometry is mathematically represented when rendering.\n", + "The nodes (vertices), edges, and faces (cells) that make up an Unstructured Grid can each be converted into a geometric shape for visualization. These geometric shapes can often be referred to as vector graphics, since each geometry is mathematically represented when rendering.\n", "\n", - "For example, in the UXarray Visualization section, we will showcase how we can convert the faces in our Unstructured Grid into Polygons.\n", + "For example, in the \"Plotting with UXarray\" chapter, we will showcase how we can convert the faces in our Unstructured Grid into Polygons.\n", "\n", - "When constructing our visualization, we can render each face using directly onto the screen. \n", + "When constructing unstructured grids visualization, we can render each face directly onto the screen. \n", "\n", - "Rendering each face as a polygon will lead to visuals that look like this, which are extremely high-quality and represent the exact geometry of each face.\n", + "Rendering each face as a polygon will lead to visuals that look like this, which are high-quality since they represent the exact geometry of each face.\n", "\n", " \n", "\n", @@ -41,6 +75,7 @@ }, { "cell_type": "markdown", + "id": "b862e8ae-58e6-47cb-849b-631878b0090f", "metadata": { "collapsed": false, "jupyter": { @@ -59,6 +94,7 @@ }, { "cell_type": "markdown", + "id": "6f2fde1d-82b8-47c9-9852-0d4e7d7618ee", "metadata": { "collapsed": false, "jupyter": { @@ -71,6 +107,7 @@ }, { "cell_type": "markdown", + "id": "e5807c07-d64b-4f75-969c-bd908d6c134d", "metadata": { "collapsed": false, "jupyter": { @@ -80,14 +117,15 @@ "source": [ "One Python package which is used for representing and manipulating geometries is [Shapely](https://shapely.readthedocs.io/en/stable/manual.html).\n", "\n", - "UXarray uses Shapely paired with SpatialPandas and other packages to represent unstructured grid elements (nodes, edges, faces) as geometries for visualization.\n", + "Shapely can be paired with SpatialPandas and other packages to represent unstructured grid elements (nodes, edges, faces) as geometries for visualization.\n", "\n", "The following code snippets are basic examples of how these elements can be represented as geometries.\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, + "id": "78cbec48-6730-4562-80a5-ffc20515d087", "metadata": { "collapsed": false, "jupyter": { @@ -101,6 +139,7 @@ }, { "cell_type": "markdown", + "id": "91edd03c-c30e-4635-b9dd-1be2a59fff7a", "metadata": { "collapsed": false, "jupyter": { @@ -113,20 +152,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, + "id": "6a5942f1-9e27-470f-859d-9f9bcd458963", "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "sp.Point([0.0, 0.0])" ] }, { "cell_type": "markdown", + "id": "4a4783e9-eccd-450b-91c6-fe0bfda39123", "metadata": { "collapsed": false, "jupyter": { @@ -139,20 +194,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, + "id": "e5ad35dd-1899-4495-ab94-11f75be3f1d6", "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "sp.LineString([[0.0, 0.0], [180, -90]])" ] }, { "cell_type": "markdown", + "id": "ac6ee958-4b5e-4327-b466-f3c2dbdba2f4", "metadata": { "collapsed": false, "jupyter": { @@ -165,30 +236,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, + "id": "575edb2b-adf1-400c-ab8b-d6357b3aacb1", "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "sp.Polygon([[100, 40], [100, 50], [90, 50], [90, 40], [100, 40]])" ] }, { "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [] - }, - { - "cell_type": "markdown", + "id": "4bfdd46f-5810-43d6-b7d6-b277119d6dd4", "metadata": { "collapsed": false, "jupyter": { @@ -198,7 +275,7 @@ "source": [ "## Rasterization\n", "\n", - "While there is definitely merit in rendering each geometric shape directly, this operation is extremely computationally expensive for large datasets.\n", + "While there is definitely merit in rendering each geometric shape directly, this operation is computationally expensive for large datasets.\n", "\n", "Rasterization is a technique in computer graphics that converts vector (a.k.a geometric shapes) graphics into a raster image, which can be thought of as a regularly-sampled array of pixel values used for rendering.\n", "\n", @@ -213,6 +290,7 @@ }, { "cell_type": "markdown", + "id": "c081ea7a-a033-46e9-baf5-d52b1be4a9db", "metadata": { "collapsed": false, "jupyter": { @@ -231,6 +309,7 @@ }, { "cell_type": "markdown", + "id": "709e29b3-da78-4a00-b595-864af151905e", "metadata": { "collapsed": false, "jupyter": { @@ -245,6 +324,23 @@ }, { "cell_type": "markdown", + "id": "563f167c-9c2f-4ddc-a02f-8e40e5fba956", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "
\n", + "

Note:

\n", + " The selection between vector graphics and rasterization needs to be made taking into account several factors such as how large is the dataset (i.e. how fine-resolution the data is), what data fidelity with the visualization is desired, what performance is expected, etc.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "1e486b87-54ad-476a-b724-398d989e8934", "metadata": { "collapsed": false, "jupyter": { @@ -257,6 +353,21 @@ " A more comprehensive showcase of rasterization can be found here\n", "" ] + }, + { + "cell_type": "markdown", + "id": "5d8786a7-8059-4fff-9af5-fba3b80ec8a3", + "metadata": { + "panel-layout": { + "height": 84.3924, + "visible": true, + "width": 100 + } + }, + "source": [ + "## What is next?\n", + "Up next is the new chapter that will provide an Introduction to UXarray." + ] } ], "metadata": { @@ -275,64 +386,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false + "version": "3.12.6" + } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/notebooks/01-foundations/unstructured-grids.ipynb b/notebooks/01-foundations/unstructured-grids.ipynb new file mode 100644 index 00000000..7d3ae144 --- /dev/null +++ b/notebooks/01-foundations/unstructured-grids.ipynb @@ -0,0 +1,536 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b0e265cab6491374", + "metadata": { + "panel-layout": { + "height": 474.08, + "visible": true, + "width": 100 + } + }, + "source": [ + "\"UXarray\n", + "\n", + "# Unstructured Grids Overview\n", + "\n", + "### In this section, you'll learn:\n", + "\n", + "* What unstructured meshes are and how they are different than structured grids\n", + "* Grid terminology and conventions\n", + "* Unstructured grid formats \n", + "\n", + "\n", + "### Related Documentation\n", + "\n", + "* [UXarray Terminology Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/terminology.html)\n", + "* [UXarray Conventions Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/representation.html)\n", + "* [UXarray Supported Models and Grid Formats Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/grid-formats.html)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [CF Conventions](https://cfconventions.org/) | Necessary | |\n", + "| [UGRID Conventions](https://ugrid-conventions.github.io/ugrid-conventions/) | Helpful | |\n", + "| [Numpy](https://numpy.org/) | Helpful | |\n", + "| [Xarray](https://docs.xarray.dev/en/stable/) | Helpful | |\n", + "\n", + "**Time to learn**: 15 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "b028cd75-5332-4482-b2ea-d5c6e2c988f7", + "metadata": { + "panel-layout": { + "height": 50.816, + "visible": true, + "width": 100 + } + }, + "source": [ + "## Structured vs Unstructured Grids" + ] + }, + { + "cell_type": "markdown", + "id": "5eb87890-358d-4563-a805-0b1b5ad6931d", + "metadata": { + "panel-layout": { + "height": 160.799, + "visible": true, + "width": 100 + } + }, + "source": [ + "Before diving into unstructured grids, it is helpful to understand the basic differences between structured and unstructured grids:\n", + "\n", + "* A structured grid is composed of grid cells (faces) that are well-organized in a predictable, grid-like topology, where simple indexing can be used to identify neighboring cells,\n", + "* An unstructured grid represents the globe as a tesselation of various polygons (e.g. triangles, hexagons, etc.) to represent complex geometries, allowing those elements to be joined in any manner, but requiring explicit information to identify neighboring elements such as cells and nodes (vertices).\n", + "\n", + "Note that the focus here is on the horizontally unstructured grids in the climate and weather context, which may or may not involve vertical levels, but the same concepts apply to three dimensional unstructured grids as well." + ] + }, + { + "cell_type": "markdown", + "id": "c1ad3ec9-eb8e-4b57-bb97-07690932bd7f", + "metadata": {}, + "source": [ + "Examples of structured grids (on left) and unstructured grids (center and right) can be seen below: " + ] + }, + { + "cell_type": "markdown", + "id": "ddeaad99-ed30-42ca-961e-9d9582299c56", + "metadata": {}, + "source": [ + "\"Alt" + ] + }, + { + "cell_type": "markdown", + "id": "cec26533-8913-47f0-b02c-b1e9824ea1b4", + "metadata": { + "panel-layout": { + "height": 44.0799, + "visible": true, + "width": 100 + } + }, + "source": [ + "### Structured Grids" + ] + }, + { + "cell_type": "markdown", + "id": "6fb5becb-3e2e-4571-b276-b958f138adf0", + "metadata": { + "panel-layout": { + "height": 196.788, + "visible": true, + "width": 100 + } + }, + "source": [ + "A few advantages of structured grids are:\n", + "- Uniform Representation: Simplifies numerical methods and enhances result interpretation.\n", + " \n", + "- Efficient Numerics: Well-suited for finite-difference schemes, ensuring computational efficiency.\n", + " \n", + "- Simplified Interpolation: Straightforward interpolation facilitates integration of observational data and model outputs.\n", + " \n", + "- Boundary Handling: Ideal for regular boundaries, easing implementation of boundary conditions.\n", + " \n", + "- Optimized Parallel Computing: Regular structure supports efficient parallel computing for scalability." + ] + }, + { + "cell_type": "markdown", + "id": "7dff95f3-9264-451c-a4ca-321f63e2f05c", + "metadata": { + "panel-layout": { + "height": 43.2118, + "visible": true, + "width": 100 + } + }, + "source": [ + "#### Sample Xarray Code to Generate a Basic Structured Grid" + ] + }, + { + "cell_type": "markdown", + "id": "163dcaef-7353-4bd8-815a-58cba735c697", + "metadata": { + "panel-layout": { + "height": 148.802, + "visible": true, + "width": 100 + } + }, + "source": [ + "The code below shows the creation of a structured grid example over 2D Earth geometry and plots random temperature data over it. There are several ways and tools to create a structured grid, but this code works as follows:\n", + "\n", + "Given the number of points in longitude and latitude directions, the code uses Numpy's meshgrid to generate a structured grid. The temperature data is then interpolated onto this grid, creating a smooth representation. Xarray is leveraged to organize the gridded data into a dataset, facilitating easy manipulation and visualization. The resulting plot showcases the data on this structured grid, providing a clearer understanding of temperature variations across defined longitude and latitude ranges. Plotting the structured grid and the temperature data is done using Matplotlib along with Cartopy, a cartographic plotting library. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1b2487ab-b16c-47ee-9755-a1c331bc619d", + "metadata": { + "panel-layout": { + "height": 700.99, + "visible": true, + "width": 100 + } + }, + "outputs": [ + { + "data": { + "image/png": "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yUiEe/WxIiIiIiJmSCQSRo4cyXffffeopyLyBJKRkYGDgwMXRvTCTlUwZlNJyczXEvLDWtLT00ukQX1SETUlIiIiIiIiFYwYZr5kPBurFBEREREREXnsETUlIiIiIsUgWrhFygMxTknJEIUSERERERGRCkY035QMUSgRERERERGpYCTSsmk7JM+GTCL6lIiIiIiIiIg8HoiaEhERERERkQpG9CkpGaJQIiIiIiIiUtFIpcZPWdo/AzwbqxQRERERERF57HkshZLdu3fTrVs3vL29kUgkrFu3zux4VlYWo0aNwtfXF7VaTUhISIHEa/n5+bz99tu4urpiY2ND9+7duXHjhlmdAwcOULduXQICAvjxxx8relmFUtxatVotEyZMoFatWtjY2ODt7c0rr7zCzZs3zfooy1qbNGnCm2++aVZ3wYIFSCQSfv75Z7PyoUOH0qxZs3Ja+YOJjY1l4MCBuLi4YG1tTd26dTl27JjpuCAITJ06FW9vb9RqNa1ateLcuXNmfURERNC8eXN8fX2ZNm2aqbxfv348//zzZnW3bNmCRCJhypQpZuXTp083SyT4uDN//nwqVaqElZUVYWFh7Nmzx3QsPj6e559/Hm9vb9566y0MBsMjnKnlVOS1AcbcSRKJpMBn9uzZD2V9hfGo7xVAoedEIpE8MAdQRfMkPDOKOnel+TwLPJZCSXZ2NnXq1CkyrPPYsWPZunUrv//+OxcuXGDs2LG8/fbbrF+/3lRnzJgxrF27lhUrVrB3716ysrLo2rUrer3eVGfIkCFMmTKF5cuX89lnnxEdHV3ha7uf4taak5PD8ePHmTJlCsePH2fNmjVcunSJ7t27m9Ury1pbt27Njh07zPrbuXMnfn5+hZa3bt26vJZeLKmpqTRv3hyFQsGWLVs4f/48c+fONUtLP2fOHL788ku+++47jhw5gqenJ+3btyczM9NUZ+TIkQwaNIj169fz119/sW/fPsC47r1795pl4X0c1l1WVq5cyZgxY/jwww85ceIEzz33HM8//7zp+548eTINGzZky5YtXL9+neXLlz/iGZeeir427jBt2jTi4uLMPm+//fbDWmYBHvW94g6LFy8ucF569uxZIWsuKU/CM0PMElxChMccQFi7dq1ZWY0aNYRp06aZldWvX1+YPHmyIAiCkJaWJigUCmHFihWm47GxsYJUKhW2bt1q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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import cartopy.crs as ccrs\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "# Define the global domain\n", + "lat_range = [-90.0, 90.0]\n", + "lon_range = [-180.0, 180.0]\n", + "\n", + "# Create a structured grid. Note the number of points in each dimension\n", + "# There is not need to store the grid points in a separate array\n", + "# Also note that the grid points are evenly spaced and not connectivity information is needed\n", + "\n", + "num_lat_points = 20\n", + "num_lon_points = 30\n", + "\n", + "lats = np.linspace(lat_range[0], lat_range[1], num_lat_points)\n", + "lons = np.linspace(lon_range[0], lon_range[1], num_lon_points)\n", + "\n", + "lons_grid, lats_grid = np.meshgrid(lons, lats)\n", + "\n", + "# Generate random temperature data for each grid point\n", + "temperature_data = np.random.uniform(\n", + " low=20, high=30, size=(num_lat_points, num_lon_points)\n", + ")\n", + "\n", + "# Create xarray Dataset\n", + "ds = xr.Dataset()\n", + "ds[\"temperature\"] = ([\"lat\", \"lon\"], temperature_data)\n", + "ds[\"lon\"] = lons\n", + "ds[\"lat\"] = lats\n", + "\n", + "# Plot the structured grid using xarray\n", + "fig, ax = plt.subplots(subplot_kw={\"projection\": ccrs.PlateCarree()})\n", + "ax.set_global()\n", + "\n", + "# Plot world map lines\n", + "ax.coastlines()\n", + "ax.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False)\n", + "\n", + "# Plot the structured grid\n", + "cs = ax.pcolormesh(\n", + " ds[\"lon\"], ds[\"lat\"], ds[\"temperature\"], cmap=\"coolwarm\", shading=\"auto\"\n", + ")\n", + "\n", + "# Colorbar\n", + "cbar = plt.colorbar(cs, ax=ax, label=\"Temperature (°C)\")\n", + "\n", + "ax.set_title(\"Structured Grid Example\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "473516a9-171d-40d8-810d-2abdddd75b84", + "metadata": { + "panel-layout": { + "height": 44.0799, + "visible": true, + "width": 100 + } + }, + "source": [ + "### Unstructured Grids" + ] + }, + { + "cell_type": "markdown", + "id": "aa7869e5-99dd-4cdb-a15f-add6c3e5949e", + "metadata": { + "panel-layout": { + "height": 51.1285, + "visible": true, + "width": 100 + } + }, + "source": [ + "Characteristic features of unstructured grids are:" + ] + }, + { + "cell_type": "markdown", + "id": "cbaf1f1a-2cff-4c3e-b5f8-93ef6d9d23bf", + "metadata": { + "panel-layout": { + "height": 196.788, + "visible": true, + "width": 100 + } + }, + "source": [ + "\n", + "- Adaptability to complex geometries: Fits intricate shapes and boundaries\n", + "\n", + "- Often runs faster than structured grids: Requires fewer elements to achieve similar accuracy\n", + " \n", + "- Local refinement: Concentrates resolution on areas of interest\n", + " \n", + "- Flexibility in element types: Accommodates various element shapes\n", + " \n", + "- Efficient parallelization: Scales well to multiple processors\n", + " \n", + "- Suitability for dynamic simulations: Adapts to changing conditions" + ] + }, + { + "cell_type": "markdown", + "id": "a0d18cd1-bc8c-4ef8-9f5c-74f2194e6700", + "metadata": { + "panel-layout": { + "height": 43.2118, + "visible": true, + "width": 100 + } + }, + "source": [ + "#### Sample Xarray Code to Generate a Basic Unstructured Mesh" + ] + }, + { + "cell_type": "markdown", + "id": "8c2ac7da-2569-4a02-aa04-5b95bbeb9882", + "metadata": { + "panel-layout": { + "height": 97.3958, + "visible": true, + "width": 100 + } + }, + "source": [ + "The code below uses a very basic, standard Python approach to showcase a simple unstructured mesh topology (without a real meaning or value). \n", + "\n", + "This code generates an unstructured grid out of 600 arbitrarily created nodes (latitude, longitude pairs sampled from corresponding ranges via a uniform distribution). It performs Delaunay triangulation of those nodes using matplotlib.tri.Triangulation to create grid cells. The resulting triangulation is then plotted using cartopy and matplotlib." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6651059f-585a-40de-93e4-a5451cd82ad7", + "metadata": { + "panel-layout": { + "height": 700.99, + "visible": true, + "width": 100 + } + }, + "outputs": [ + { + "data": { + "image/png": "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v8Xv3H777SiVSr744gvmzZuHUvnTsondeuutPPjgg/Tt25c5c+bwzTffMHny5PBgv2DBAjIzM+nZs2e4DQsWLPhZ16SyspIbb7yRWbNmsXz5cu69916cTifDhg3j888/54EHHmDFihU88cQTfPbZZ0yePDks6YuiyJQpU/jiiy949NFHWbBgAVdccQVXXXXVT27HHXfcwcKFC7FarQAUFBSwfft27rjjjja3f/nll7n++uvJyclh7ty5fPHFFzgcDoYMGcLRo0fD240fP559+/bx2muvsWbNGj744AN69uxJQ0NDi+MdPHiQRx99lIcffphFixbRrVs37rjjDjZv3nxR7RdFEb/f32ppvlYxMTF888037NmzJ5zFzel0MmPGDFJTU/nwww/Dxzp9+jQDBgzggw8+YPXq1Tz//PPs2rWLwYMHtxAqmrn22mvp3r073333HXfeeSd//etfefjhh5kyZQoTJkxgwYIFjBw5kieeeIL58+e32v/pp5+msLCQjz/+mI8//piKigqGDx9OYWHhBc/5Yu+BxOXBpeUwuLSIhv9ZRIlfjRdeeEEExNra2ja/79Klizhs2LDw5w0bNoiAOH78+BbbzZ07VwTEHTt2iKIoinv37hUBceHChRf8fZ1OJ95yyy2t1n/66aciIN58882tvrvlllvEtLS0855LM5s3bxYB8ZlnnrlgG849x3PbUFRU1GJ98zXYsGFDeN2wYcNEQFy3bl2LbV955RVRJpOJe/bsabF+3rx5IiAuX75cFEVRXLFihQiIf/vb31ps96c//UkExBdeeOGC51BUVCQC4uuvvy46HA4xMjJSfPfdd0VRFMXHHntMzMjIEIPBoHjfffe1uEZnzpwRFQqF+Lvf/a7F8RwOh5iQkCBee+21oiiKYl1dnQiIb7311gXbkZaWJmo0GrGkpCS8zuVyiWazWbz77rsvuK8oiiJw3uWLL75ose2rr74qAuKCBQvEW265RdRqtWJ+fv55jx0MBkWfzyeWlJSIgLho0aLwd83PzhtvvNFinx49eoiAOH/+/PA6n88nxsbGitOmTQuva34mevXqJQaDwfD64uJiUalUir/5zW9a/VYzF3sPJP77sdlsoT7jqVvEuj/c+bOXoqduEQHRZrP9p0/pvwZJY3AZMHny5Bafu3XrBkBJSQkAHTp0wGQy8cQTT/Dhhx/+7FnP9OnTf3YbV6xYAYQ8dn8NTCYTI0eObLFu6dKl5Obm0qNHjxaz37Fjx7YwR2zYsAGAG264ocX+P8djODIykhkzZvDPf/4Tv9/Pv/71L2677bY2fTNWrVqF3+/n5ptvbtE+jUbDsGHDwu0zm820b9+e119/nTfffJO8vDyCwWCbv9+jRw9SU1PDnzUaDR07dgw/Gz/Gtddey549e1ot48ePb7HdY489xoQJE7j++uv5/PPPeeedd+jatWuLbWpqarjnnnto164dCoUCpVJJWloaAMeOHWv12+dGVHTu3BlBEFpobhQKBR06dGjzfGbNmtXiOqelpTFw4MDw/W2Li70HEhL/PyM5H/6KKBShy91sAjgXv9/fpvo+Ojq6xefmjFzN4TEGg4FNmzbxpz/9iaeffhqr1UpiYiJ33nknzz777EWbBM5OxvFTqa2tRS6Xh5N7/Ltpq63V1dWcOnXqvOfbbOuur69HoVC0uq4/t+133HEHgwcP5k9/+hO1tbXnjbiorq4GoG/fvm1+3xzZIAgC69at46WXXuK1117j0UcfxWw2c8MNN/CnP/2phWfzuecAoeej+dn4MWJjY+nTp8+Pbtccwrhs2TISEhJa+BZAyL9kzJgxVFRU8Nxzz9G1a1d0Oh3BYJArrriizfY055ZvRqVSERERgUajabW+reiOtu5XQkICBw8ePO95XOw9kLiMuFRzgGRKaIUkGPyKNGe9Ki8vb5UBSxRFKisrL6qTbouuXbvyzTffIIoi+fn5fPbZZ7z00ktotVqefPLJizpGW7NcjUbTZjGRcx3KYmNjCQQCrbJ9XSzNg8G5v3W+nA9ttTUmJgatVnte57uYmBggNJj6/X7q6+tbDKw/N4/5oEGDyM7O5qWXXmL06NG0a9fugr8/b9688Ez6fKSlpfHJJ58AcOLECebOncuLL76I1+ttYdf/taisrOS+++6jR48eHDlyhN///ve8/fbb4e8PHz7MwYMH+eyzz7jlllvC60+dOvVva1Nb96uqqqpNYamZn3IPJC4PBEGGIFxCVMIl7Pu/inRFfkVGjhyJIAjMmTOn1XcrV67Ebrdz5ZVXXtJvCIJA9+7d+etf/4rRaGT//v3h737KTLKZ9PR0ampqwjMtCGXxWrVqVYvtmtW/zTG+5+N8bWiOTsjPz2+xvjmi4WKYOHEip0+fJjo6mj59+rRamn9jxIgRAHz11Vct9v/6668v+rfO5dlnn2XSpElt5gRoZuzYsSgUCk6fPt1m+84nFHbs2JFnn32Wrl27trifvxaBQIDrr78eQRBYsWIFr7zyCu+8804Lh8BmQe3c/PJ///vf/23tmj17dovQsZKSErZv337BJGE/9x5I/BcjEy59kWiBpDH4FWnfvj33338/r7/+Og0NDYwfPx6tVsuePXv485//TJ8+fX6WnXvp0qW8//77TJkyhczMTERRZP78+TQ0NLSIr+/atSsbN25kyZIlJCYmotfryc7OvuCxZ86cyfPPP891113HY489htvt5u23325lDhkyZAg33XQTf/zjH6murmbixImo1Wry8vKIiIjgd7/7XbgN33zzDXPmzCEzMxONRkPXrl3p27cv2dnZ/P73v8fv92MymViwYEGbxUnOx0MPPcR3333H0KFDefjhh+nWrRvBYJAzZ86wevVqHn30Ufr378+YMWMYOnQojz/+OE1NTfTp04dt27bxxRdf/ISr3pIbb7yRG2+88YLbpKen89JLL/HMM89QWFjIuHHjMJlMVFdXs3v3bnQ6HX/4wx/Iz8/n/vvvZ8aMGWRlZaFSqVi/fj35+fkXrf25WKqrq9m5c2er9VFRUeTk5ADwwgsvsGXLFlavXk1CQgKPPvoomzZt4o477qBnz55kZGTQqVMn2rdvz5NPPokoipjNZpYsWdIiLewvTU1NDVOnTuXOO+/EZrPxwgsvoNFoeOqpp867z8XeAwmJ/5+RBINfmb/97W/k5OTwySef8OWXX+L3+0lLS+O+++7j2WefRaVS/eRjZmVlYTQaee2116ioqEClUpGdnd1Krfu3v/2N++67j+uuuy4c2vdjzlYZGRksWrSIp59+mmuuuYbExEQeeeQRamtrW3Wgn332Gb169eKTTz7hs88+Q6vVkpOTw9NPPx3e5g9/+AOVlZXceeedOBwO0tLSKC4uRi6Xs2TJEu6//37uuece1Go11113He+++y4TJky4qOug0+nYsmULf/7zn/noo48oKipCq9WSmprKlVdeGdYYyGQyFi9ezCOPPMJrr72G1+tl0KBBLF++nE6dOl3cRf+ZPPXUU+Tk5PC3v/2N2bNn4/F4SEhIoG/fvtxzzz1AyE7evn173n//fUpLSxEEgczMTN54442wgPVLMW/ePObNm9dq/aBBg9i6dStr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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import cartopy.crs as ccrs\n", + "import matplotlib.pyplot as plt\n", + "import matplotlib.tri as mtri\n", + "import numpy as np\n", + "import xarray as xr\n", + "\n", + "# Generate random temperature data\n", + "np.random.seed(42)\n", + "num_points = 600\n", + "latitudes = np.random.uniform(low=-90, high=90, size=num_points)\n", + "longitudes = np.random.uniform(low=-180, high=180, size=num_points)\n", + "temperatures = np.random.uniform(low=-30, high=30, size=num_points)\n", + "\n", + "# Create xarray DataArray for temperature data\n", + "temperature_data = xr.DataArray(\n", + " temperatures, dims=\"points\", coords={\"points\": range(num_points)}\n", + ")\n", + "\n", + "# Perform Delaunay triangulation\n", + "triang = mtri.Triangulation(longitudes, latitudes)\n", + "\n", + "# Create xarray DataArray for triangulation coordinates\n", + "triang_data = xr.DataArray(\n", + " np.column_stack([triang.x, triang.y]), dims=(\"points\", \"coords\")\n", + ")\n", + "\n", + "# Plot the globe with unstructured mesh using xarray\n", + "fig, ax = plt.subplots(subplot_kw={\"projection\": ccrs.PlateCarree()})\n", + "ax.set_global()\n", + "\n", + "# Plot world map lines with prominent gridlines\n", + "ax.coastlines(linewidth=0.5)\n", + "# ax.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False, linewidth=0.5)\n", + "\n", + "# Plot unstructured mesh with bold lines\n", + "ax.triplot(\n", + " triang, \"ko-\", markersize=0.1, linewidth=0.5, alpha=0.5\n", + ") # Increase linewidth to see the triangles\n", + "\n", + "# Scatter plot with temperature data\n", + "sc = ax.scatter(\n", + " longitudes,\n", + " latitudes,\n", + " c=temperature_data,\n", + " cmap=\"coolwarm\",\n", + " s=10,\n", + " transform=ccrs.PlateCarree(),\n", + ")\n", + "\n", + "# Colorbar\n", + "cbar = plt.colorbar(sc, ax=ax, label=\"Temperature (°C)\")\n", + "\n", + "ax.set_title(\"Unstructured Mesh Example\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "26ab8a93-9360-48d1-a9f6-7d00077d34e4", + "metadata": { + "panel-layout": { + "height": 85.3993, + "visible": true, + "width": 100 + } + }, + "source": [ + "
\n", + "

Note:

\n", + " This is a very basic example to show how an unstructured grid with triangles would look like. In actual model outputs, often the region of interest is meshed with a finer resolution, while it is coarsened in areas where high resolution is not needed. This is done to reduce the number of elements and improve computational efficiency.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "3d0bdae8-4980-4baf-a5de-f71cd58014d0", + "metadata": {}, + "source": [ + "## Why Unstructured Grids?" + ] + }, + { + "cell_type": "markdown", + "id": "c0bd4c67-377b-4029-86cd-1ec9bb4a2513", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "#### Arrival of the next generation, kilometer-scale global models for weather and climate\n", + "The aforementioned features and strengths of unstructured grids make them appealing for data analysis and visualization of kilometer-scale global weather and climate model outputs that produce very large spatio-temporal datasets. \n", + "\n", + "Therefore, after nearly two decades of development and evaluation, the climate and global weather modeling communities are transitioning from simple structured grids to this kind of more complex, but more scalable unstructured grids upon which governing equations of state are solved. " + ] + }, + { + "cell_type": "markdown", + "id": "90e9e0be-6847-4e7d-8a45-f7b92ff1e852", + "metadata": {}, + "source": [ + "## Many Unstructured Grid Types, No Commonly Used Convention or Tools" + ] + }, + { + "cell_type": "markdown", + "id": "3ac97daf-d889-4947-a789-7d661c805803", + "metadata": {}, + "source": [ + "There are several climate and weather models that generate unstructured grids that vary from each other regarding not only the geometric elements (shape of grid faces, i.e. polygons, etc.) but also the file formats and extensions, and how special information such as geometric elements' connectivity is represented. This creates a challenge towards having a widely used convention for the representation of unstructured grid data.\n", + "\n", + "There are only a few analysis tools that are capable of working directly with unstructured mesh data that come from so many mesh formats, and most of the time, the common practice is to resample to structured grids, but this has myriad pitfalls. " + ] + }, + { + "cell_type": "markdown", + "id": "d4ee424b-b9bd-46aa-b4ca-fe029df2b715", + "metadata": { + "panel-layout": { + "height": 305.451, + "visible": true, + "width": 100 + } + }, + "source": [ + "## Why UXarray for Unstructured Grids?\n", + "\n", + "UXarray, which stands for \"Unstructured Grids - Xarray\", is a Python package that provides Xarray-styled analysis and visualization functionality for working **directly** with unstructured grids that are resulting from most of the climate and weather models. Offering a unified representation of various model outputs around the UGRID conventions, UXarray enables working with native unstructured grids in a single, grid format-agnostic interface.\n", + "\n", + "More on UXarray in the next chapter." + ] + }, + { + "cell_type": "markdown", + "id": "aa2b87e5-fcd7-4aac-96a7-dbcea032b780", + "metadata": { + "panel-layout": { + "height": 84.3924, + "visible": true, + "width": 100 + } + }, + "source": [ + "## What is next?\n", + "The next section will provide an overview of the general-purpose plotting libraries available in the scientific Python ecosystem." + ] + } + ], + "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.8" + }, + "panel-cell-order": [ + "b0e265cab6491374", + "b028cd75-5332-4482-b2ea-d5c6e2c988f7", + "5eb87890-358d-4563-a805-0b1b5ad6931d", + "cec26533-8913-47f0-b02c-b1e9824ea1b4", + "6fb5becb-3e2e-4571-b276-b958f138adf0", + "7dff95f3-9264-451c-a4ca-321f63e2f05c", + "163dcaef-7353-4bd8-815a-58cba735c697", + "1b2487ab-b16c-47ee-9755-a1c331bc619d", + "473516a9-171d-40d8-810d-2abdddd75b84", + "aa7869e5-99dd-4cdb-a15f-add6c3e5949e", + "cbaf1f1a-2cff-4c3e-b5f8-93ef6d9d23bf", + "a0d18cd1-bc8c-4ef8-9f5c-74f2194e6700", + "8c2ac7da-2569-4a02-aa04-5b95bbeb9882", + "6651059f-585a-40de-93e4-a5451cd82ad7", + "26ab8a93-9360-48d1-a9f6-7d00077d34e4", + "d4ee424b-b9bd-46aa-b4ca-fe029df2b715", + "52651809-c7d0-4aee-b0fd-ec73b590ab3e", + "aa2b87e5-fcd7-4aac-96a7-dbcea032b780" + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/01-intro/01-unstructured-grid-overview.ipynb b/notebooks/01-intro/01-unstructured-grid-overview.ipynb deleted file mode 100644 index 34ed0039..00000000 --- a/notebooks/01-intro/01-unstructured-grid-overview.ipynb +++ /dev/null @@ -1,372 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Unstructured Grids Overview" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The goal of this notebook is to provide a brief overview of unstructured grids and provide a teaser of plotting with the [UXarray](https://uxarray.readthedocs.io/) package.\n", - "\n", - "Contents:\n", - "1. Structured vs. Unstructured Grids\n", - "2. Structured Grids\n", - "3. Unstructured Grids\n", - "4. Why UXarrary for unstructured grids?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Structured vs Unstructured Grids\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It is important to understand this difference, before diving into unstructured grid:\n", - "\n", - "A structured grid is well-ordered, with a simple scheme used to label elements and identify neighbors, while an unstructured grid allows elements to be joined in any manner, requiring special lists to identify neighboring elements.\n", - "\n", - "Note that the focus here is on two dimensional grids in the climate and weather context, but the same concepts apply to three dimensional grids." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Structured Grids\n", - "A few advantages of structured grids are:\n", - "- Uniform Representation: Simplifies numerical methods and enhances result interpretation.\n", - " \n", - "- Efficient Numerics: Well-suited for finite-difference schemes, ensuring computational efficiency.\n", - " \n", - "- Simplified Interpolation: Straightforward interpolation facilitates integration of observational data and model outputs.\n", - " \n", - "- Boundary Handling: Ideal for regular boundaries, easing implementation of boundary conditions.\n", - " \n", - "- Optimized Parallel Computing: Regular structure supports efficient parallel computing for scalability." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This code below shows a structured mesh example over 2D earth geometry and plots random temperature data. \n", - "\n", - "Given the number of points in longitude and latitude direction, the code uses Numpy's meshgrid to generates a structured grid. The temperature data is then interpolated onto this grid, creating a smooth representation. Xarray is leveraged to organize the gridded data into a dataset, facilitating easy manipulation and visualization. The resulting plot showcases the data on this structure mesh, providing a clearer understanding of temperature variations across defined longitude and latitude ranges. Plotting the structured grid and the temperature data is done using Cartopy, a cartographic plotting library. \n", - "\n", - "There are many other libraries and ways to create a structured grids." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import cartopy.crs as ccrs\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import xarray as xr\n", - "\n", - "# Define the global domain\n", - "lat_range = [-90.0, 90.0]\n", - "lon_range = [-180.0, 180.0]\n", - "\n", - "# Create a structured grid. Note the number of points in each dimension\n", - "# There is not need to store the grid points in a separate array\n", - "# Also note that the grid points are evenly spaced and not connectivity information is needed\n", - "\n", - "num_lat_points = 20\n", - "num_lon_points = 30\n", - "\n", - "lats = np.linspace(lat_range[0], lat_range[1], num_lat_points)\n", - "lons = np.linspace(lon_range[0], lon_range[1], num_lon_points)\n", - "\n", - "lons_grid, lats_grid = np.meshgrid(lons, lats)\n", - "\n", - "# Generate random temperature data for each grid point\n", - "temperature_data = np.random.uniform(\n", - " low=20, high=30, size=(num_lat_points, num_lon_points)\n", - ")\n", - "\n", - "# Create xarray Dataset\n", - "ds = xr.Dataset()\n", - "ds[\"temperature\"] = ([\"lat\", \"lon\"], temperature_data)\n", - "ds[\"lon\"] = lons\n", - "ds[\"lat\"] = lats\n", - "\n", - "# Plot the structured grid using xarray\n", - "fig, ax = plt.subplots(subplot_kw={\"projection\": ccrs.PlateCarree()})\n", - "ax.set_global()\n", - "\n", - "# Plot world map lines\n", - "ax.coastlines()\n", - "ax.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False)\n", - "\n", - "# Plot the structured grid\n", - "cs = ax.pcolormesh(\n", - " ds[\"lon\"], ds[\"lat\"], ds[\"temperature\"], cmap=\"coolwarm\", shading=\"auto\"\n", - ")\n", - "\n", - "# Colorbar\n", - "cbar = plt.colorbar(cs, ax=ax, label=\"Temperature (°C)\")\n", - "\n", - "ax.set_title(\"Structured Grid Example\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Unstructured Grids" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Characteristic features of unstructured grids are:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "- Adaptability to complex geometries: Fits intricate shapes and boundaries\n", - "\n", - "- Often runs faster than structured grids: Requires fewer elements to achieve similar accuracy\n", - " \n", - "- Local refinement: Concentrates resolution on areas of interest\n", - " \n", - "- Flexibility in element types: Accommodates various element shapes\n", - " \n", - "- Efficient parallelization: Scales well to multiple processors\n", - " \n", - "- Suitability for dynamic simulations: Adapts to changing conditions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are a variety of libraries and conventions for creating unstructured grids. Here we use a very basic standard python approach to showcase an unstructured grid: \n", - "\n", - "The code generates an unstructured grid over a rectangular domain defined by latitude and longitude ranges. It creates a set of points using matplotlib.tri.Triangulation. The resulting triangulation is then plotted using cartopy and matplotlib.\n", - "\n", - "The are many other types of elements that can be used to create unstructured grids. Polygonal elements are often used to represent complex geometries. Meshes often contain mixed elements with different types of elements used in different areas of the domain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import cartopy.crs as ccrs\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib.tri as mtri\n", - "import numpy as np\n", - "import xarray as xr\n", - "\n", - "# Generate random temperature data\n", - "np.random.seed(42)\n", - "num_points = 600\n", - "latitudes = np.random.uniform(low=-90, high=90, size=num_points)\n", - "longitudes = np.random.uniform(low=-180, high=180, size=num_points)\n", - "temperatures = np.random.uniform(low=-30, high=30, size=num_points)\n", - "\n", - "# Create xarray DataArray for temperature data\n", - "temperature_data = xr.DataArray(\n", - " temperatures, dims=\"points\", coords={\"points\": range(num_points)}\n", - ")\n", - "\n", - "# Perform Delaunay triangulation\n", - "triang = mtri.Triangulation(longitudes, latitudes)\n", - "\n", - "# Create xarray DataArray for triangulation coordinates\n", - "triang_data = xr.DataArray(\n", - " np.column_stack([triang.x, triang.y]), dims=(\"points\", \"coords\")\n", - ")\n", - "\n", - "# Plot the globe with unstructured mesh using xarray\n", - "fig, ax = plt.subplots(subplot_kw={\"projection\": ccrs.PlateCarree()})\n", - "ax.set_global()\n", - "\n", - "# Plot world map lines with prominent gridlines\n", - "ax.coastlines(linewidth=0.5)\n", - "# ax.gridlines(draw_labels=True, dms=True, x_inline=False, y_inline=False, linewidth=0.5)\n", - "\n", - "# Plot unstructured mesh with bold lines\n", - "ax.triplot(\n", - " triang, \"ko-\", markersize=0.1, linewidth=0.5, alpha=0.5\n", - ") # Increase linewidth to see the triangles\n", - "\n", - "# Scatter plot with temperature data\n", - "sc = ax.scatter(\n", - " longitudes,\n", - " latitudes,\n", - " c=temperature_data,\n", - " cmap=\"coolwarm\",\n", - " s=10,\n", - " transform=ccrs.PlateCarree(),\n", - ")\n", - "\n", - "# Colorbar\n", - "cbar = plt.colorbar(sc, ax=ax, label=\"Temperature (°C)\")\n", - "\n", - "ax.set_title(\"Unstructured Grid Example\")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Note:

\n", - " This is a very basic example of an unstructured grid with triangles. There are very specialized libraries to create\n", - " unstructured grids. Often the region of interest is meshed with a finer resolution. The mesh is then coarsened in\n", - " areas where the resolution is not needed. This is done to reduce the number of elements and improve computational\n", - " efficiency.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Why UXarray for Unstructured Grids?\n", - "\n", - "UXarray, which stands for \"Unstructured-Xarray\", is a Python package that provides Xarray-styled functionality for working with unstructured grids built around the UGRID conventions. UXarray can simplify working with unstructured grids because it:\n", - "\n", - "- Enables significant data analysis and visualization functionality to be executed directly on unstructured grids\n", - "\n", - "- Adheres to the UGRID specifications for compatibility across a variety of mesh formats\n", - "\n", - "- Provides a single interface for supporting a variety of unstructured grid formats including UGRID, MPAS, SCRIP, and Exodus\n", - "\n", - "- Inherits from Xarray, providing simplified data using familiar (Xarray-like) data structures and operations\n", - " \n", - "- Brings standardization to unstructured mesh support for climate data analysis and visualization\n", - "\n", - "- Builds on optimized data structures and algorithms for handling large and complex unstructured datasets\n", - "\n", - "- Supports enhanced interoperability and community collaboration" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Note:

\n", - " This notebook serves as an introduction to unstructured grids and UXarray. For more information, please visit the\n", - " UXarray documentation and specifically see the \n", - " Usage examples section.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## What is next?\n", - "The next sections will start with basic building blocks of UXarray and then slowly dive into more advanced features." - ] - } - ], - "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.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/01-intro/02-data-structures.ipynb b/notebooks/01-intro/02-data-structures.ipynb deleted file mode 100644 index d48f6178..00000000 --- a/notebooks/01-intro/02-data-structures.ipynb +++ /dev/null @@ -1,328 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Data Structures\n", - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "UXarray extends upon Xarray's core data structures (i.e. Dataset and DataArray) and provides functionality for operating on unstructured (a.k.a. non-regular) grids. \n", - "\n", - "This notebook will showcase the core UXarray data structures and how to interact with them" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import uxarray as ux" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "First, lets specify the paths to our Grid and Data files. As mentioned in the previous notebook, unstructured grids are typically separated into two files: one containing the grid topology and one containing data variables that reside on that grid." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "file_dir = \"../../meshfiles/\"\n", - "grid_filename = file_dir + \"oQU480.grid.nc\"\n", - "data_filename = file_dir + \"oQU480.data.nc\"" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## `Grid` Data Structure\n", - "\n", - "An unstructured grid file can be opened standalone using the `ux.open_grid` method, which is a catch-all method that parses the provided input and returns a `Grid` object with grid coordinate and connectivity variables represented in the UGRID conventions.\n", - "\n", - "Printing our `Grid` instance shows all available grid variables and original grid type.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid = ux.open_grid(grid_filename)\n", - "grid" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "We can access our coordinate, connectivity, and other descriptor variables as attributes through this `Grid` instance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid.node_lon" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid.face_node_connectivity" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## `UxDataset` & `UxDataArray` Data Structures\n", - "\n", - "UXarray inherits from Xarray's two core data structures `Dataset` & `DataArray` to provide a grid-informed implementation through the `UxDataset` & `UxDataArray` data structures. \n", - "\n", - "The major difference between them is that UXarray's implementation is paired with a `Grid` object, accessed through the `.uxgrid` property.\n", - "\n", - "UXarray also provides an overloaded `ux.open_dataset` method, which takes in both a Grid and Data file path to construct a `UxDataset`\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds = ux.open_dataset(grid_filename, data_filename)\n", - "uxds" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "We can see that our `UxDataset` has a single data variable ``bottomDepth``, which is mapped to each face (as specified by the ``n_face`` dimension).\n", - "\n", - "We can access this variable by indexing our `UxDataset` to obtain a `UxDataArray`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "You can access the `Grid` instance using the `.uxgrid` attribute, which is linked to every `UxDataset` and `UxDataArray`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds.uxgrid" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "Each `UxDataArray` under a `UxDataset` is linked to the same `Grid` object" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds.uxgrid == uxds[\"bottomDepth\"].uxgrid" - ] - } - ], - "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.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/01-intro/03-data-mapping.ipynb b/notebooks/01-intro/03-data-mapping.ipynb deleted file mode 100644 index 04c5d261..00000000 --- a/notebooks/01-intro/03-data-mapping.ipynb +++ /dev/null @@ -1,272 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Data Mapping\n", - "---\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "When attempting to visualize a data variable that resides on an unstructured grid, it's important to identify what element it is mapped to, since that will dictate what visualization to choose.\n", - "\n", - "This notebook provides a quick overview of how data is commonly mapped to unstructured grid elements." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import uxarray as ux" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "is_executing": true, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Sample Mesh\n", - "\n", - "As described in the previous sections, an Unstructured Grid is composed of Nodes, Edges, and Faces.\n", - "\n", - "Below is a basic example of an Unstructured Grid, containing 13 Nodes, 15 Edges, and 3 Faces.\n", - "\n", - "![Sample Mesh](../images/sample/sample_mesh.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Face-Centered Data\n", - "\n", - "Face-Centered data is mapped to the area that each face covers. \n", - "\n", - "![Faces](../images/sample/faces.png)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Node-Centered Data\n", - "\n", - "Node-Centered data is assigned to the corners of each face.\n", - "\n", - "\n", - "![Faces](../images/sample/nodes.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Edge-Centered Data\n", - "\n", - "Edge-Centered data is assigned to the edge that connects each pair of modes.\n", - "\n", - "![Edges](../images/sample/edges.jpg)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Identifying Data Mappings with UXarray\n", - "\n", - "We can identify what element a data variable is mapped to by looking at the final dimensions of a `UxDataArray` or `UxDataset`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "file_dir = \"../../meshfiles/\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid_filename_mpas = file_dir + \"oQU480.grid.nc\"\n", - "data_filename_mpas = file_dir + \"oQU480.data.nc\"\n", - "uxds_mpas = ux.open_dataset(grid_filename_mpas, data_filename_mpas)\n", - "\n", - "uxds_mpas[\"bottomDepth\"].dims" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The variable ``bottomDepth`` has a dimension of ``n_face``, which means that it is mapped to faces." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid_filename_geoflow = file_dir + \"geoflow.grid.nc\"\n", - "data_filename_geoflow = file_dir + \"geoflow.data.nc\"\n", - "uxds_geoflow = ux.open_dataset(grid_filename_geoflow, data_filename_geoflow)\n", - "\n", - "uxds_geoflow[\"v1\"].dims" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The variable ``v1`` has a final dimension of ``n_node``, which means that it is mapped to the corner nodes of each face. However, it also has additional dimensions, ``time`` and ``meshLayers``. These additional dimensions describe the dimensionality of the data outside the unstructured grid, representing the temporal and vertical dimensions." - ] - } - ], - "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.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/02-intro-to-uxarray/grid.ipynb b/notebooks/02-intro-to-uxarray/grid.ipynb new file mode 100644 index 00000000..566aae62 --- /dev/null +++ b/notebooks/02-intro-to-uxarray/grid.ipynb @@ -0,0 +1,293 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "08515bef-ae48-42ce-9a00-6850219e7a61", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# UXarray Grid\n", + "\n", + "### In this tutorial, you'll learn:\n", + "\n", + "* What is a UXarray `Grid`?\n", + "* How to load a `Grid`\n", + "* How to access `Grid` attributes\n", + "\n", + "### Related Documentation\n", + "\n", + "* [UXarray Data Structures Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/data-structures.html)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| Unstructured Grids | Necessary | |\n", + "| Python programming | Necessary | |\n", + "| [Introduction to Xarray](https://foundations.projectpythia.org/core/xarray/xarray-intro.html) | Helpful | |\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "01a6f9ae-5f4d-4465-8382-774940052bb6", + "metadata": {}, + "source": [ + "## Overview" + ] + }, + { + "cell_type": "markdown", + "id": "a2041dc5-06c0-4cae-bbfb-4f62bfb3b7ab", + "metadata": {}, + "source": "In the previous section, we briefly introduced the ``Grid`` class, which stores unstructured grid variables such as coordinates and connectivity. This class is the foundation of UXarray, which ensures awareness of the unstructured grid topology between operations. Exploring the grid geometry can be helpful throughout analysis and visualization workflows. \n" + }, + { + "cell_type": "markdown", + "id": "aa1771c8-d5b9-4eb2-8cc5-961c214a7625", + "metadata": {}, + "source": [ + "
\n", + "

Note:

\n", + " In most cases, checking the ``Grid`` object of either ``UxDataset`` or ``UxDataArray`` is anticipated be the common scenario since majority of the UXarray workflows will rely on a data set and its variable(s) of interest. Such cases will be showcased as part of many of the following notebooks; thus, this tutorial will focus on the latter where we explore a standalone ``Grid`` object.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "7482e47b-4feb-44d7-b118-221ca3d1b50f", + "metadata": {}, + "source": [ + "## Unstructured Grid Files" + ] + }, + { + "cell_type": "markdown", + "id": "13319a80-c211-43b5-b7d1-dad74d4b6d94", + "metadata": {}, + "source": "In the following sections, we will look into loading a standalone grid-specific file. The coordinates and connectivity variables of an unstructured grid are often stored as a separate unstructured grid file. " + }, + { + "cell_type": "code", + "id": "90c3f4f3-bf5c-41cc-8122-e410b4cb8559", + "metadata": {}, + "source": [ + "grid_path = \"../../meshfiles/outCSne30.grid.ug\"" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "e1d46358-70ae-4500-9786-d9e986637029", + "metadata": {}, + "source": [ + "## Loading a Grid" + ] + }, + { + "cell_type": "markdown", + "id": "4e5f564a-3fb2-4fcc-b010-94a2631011a8", + "metadata": {}, + "source": "The suggested way to construct a **standalone** `Grid` is by using the `uxarray.open_grid()` method. When constructing a standalone grid, only topology variables such as coordinates and connectivity are used to construct the grid. This means that any data variables that reside on the unstructured grid, such as temperature, would not be stored in a `Grid`. Pairing the grid definiton with data variables will be covered in the next notebook. \n" + }, + { + "cell_type": "code", + "id": "6bdf1449fbe884c4", + "metadata": {}, + "source": [ + "import uxarray as ux\n", + "\n", + "uxgrid = ux.open_grid(grid_path)\n", + "uxgrid" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Printing a `Grid` displays the contents of our newly created grid object. If you are coming from an Xarray background, this output will look very similar to what one might see when constructing an `xarray.Dataset`. ", + "id": "df239b4dac17e91f" + }, + { + "cell_type": "markdown", + "id": "a5a24484-e5c3-4d38-829f-359d9b3a23cb", + "metadata": {}, + "source": [ + "## Coordinates" + ] + }, + { + "cell_type": "markdown", + "id": "fefc4cad-d68e-4385-8b16-ecf32cc00185", + "metadata": {}, + "source": "Different types of coordinates for the geometric elements of a ``Grid`` are either constructed upfront during the grid instantiation or generated at the time of use." + }, + { + "cell_type": "markdown", + "id": "89eea29c-0f12-4c19-9dac-b5801abd57b8", + "metadata": {}, + "source": [ + "### Spherical Coordinates" + ] + }, + { + "cell_type": "markdown", + "id": "bb0b46d3-9556-4bc9-b9b2-9f247d6e85e2", + "metadata": {}, + "source": [ + "The spherical **node** coordinates are always instantiated upfront with the grid initialization and can be examined through the grid attributes such as ``node_lon``, ``node_lat``, etc:" + ] + }, + { + "cell_type": "code", + "id": "caae6cef-bd2c-4a6a-b089-010e5bbfca96", + "metadata": {}, + "source": [ + "uxgrid.node_lon" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "3f564529-c66a-4bdd-844d-a48a21b46801", + "metadata": {}, + "source": [ + "However, the spherical **face** and **edge** coordinates might not be readily available in the grid definition (see the original ``Grid`` object above for reference) and would need to be generated by UXarray when prompted; for instance: " + ] + }, + { + "cell_type": "code", + "id": "957b37d6-aba1-413b-83a6-13b4ed885675", + "metadata": {}, + "source": [ + "uxgrid.face_lon" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "2e285ba6-db3a-45bd-98b8-a0fe034041fc", + "metadata": {}, + "source": [ + "### Cartesian coordinates" + ] + }, + { + "cell_type": "markdown", + "id": "02589a10-3696-46c2-bf66-611f3204cbb1", + "metadata": {}, + "source": [ + "The original ``uxgrid`` object above shows no Cartesian coordinates; however, UXarray is able to generate those as soon as the user tries to access one of them; for instance:" + ] + }, + { + "cell_type": "code", + "id": "d0c9074b-56f8-41bd-856a-41747dec1b9b", + "metadata": {}, + "source": [ + "uxgrid.node_x" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "a32161a1-b48c-472b-9450-bbdaa33f3c90", + "metadata": {}, + "source": [ + "## Connectivity" + ] + }, + { + "cell_type": "markdown", + "id": "f8e5bca1-ea3d-46ea-bb4b-354f48576690", + "metadata": {}, + "source": [ + "In unstructured grid geometry, connectivity information between each type of geometric elements can be defined, e.g. face-node, node-edge, edge-face, etc. UXarray requires only the **face-node** connectivity, either coming from the grid definition or being constructed by UXarray in special cases, in addition to the node coordinates to represent the topology. \n", + "\n", + "UXarray can also generate all the other connectivity information when prompted.\n", + "\n", + "Let us look at the face-node connectivity, for example:" + ] + }, + { + "cell_type": "code", + "id": "50456578-bd38-440a-810e-825ef80df0dd", + "metadata": {}, + "source": [ + "uxgrid.face_node_connectivity" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "be51d046-0ea7-46e4-b27a-288d072e8f2e", + "metadata": {}, + "source": [ + "## Descriptors" + ] + }, + { + "cell_type": "markdown", + "id": "f7420393-9c75-4e1a-b2ae-f4221bccaa79", + "metadata": {}, + "source": [ + "UXarray provides descriptors for further information about the geometry. For instance, an array that shows the number of nodes for each face can be helpful to determine if the grid is uniformly constructed of a single shape or multiple n-gons, and simplify some of the grid-specific calculations as well." + ] + }, + { + "cell_type": "code", + "id": "9acde2005fcdb175", + "metadata": {}, + "source": [ + "uxgrid.n_nodes_per_face" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "036d6bab-d89c-481c-94cf-8e0f867ebeac", + "metadata": {}, + "source": [ + "## What is next?\n", + "The next section will cover the other core data structures of UXarray, ``UxDataset`` and ``UxDataArray`` classes and how to open unstructured grid datasets for data analysis and visualization purposes." + ] + } + ], + "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.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/02-intro-to-uxarray/overview.ipynb b/notebooks/02-intro-to-uxarray/overview.ipynb new file mode 100644 index 00000000..58d7a19b --- /dev/null +++ b/notebooks/02-intro-to-uxarray/overview.ipynb @@ -0,0 +1,189 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1cec8962-4c94-405f-b8d9-fb24d52383bd", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# Overview\n", + "\n", + "### In this section, you'll learn:\n", + "\n", + "* What is UXarray?\n", + "* Why UXarray?\n", + "* How UXarray is designed\n", + "* UXarray Core API to work with unstructured grids\n", + "\n", + "### Related Documentation\n", + "\n", + "* [UXarray Homepage](https://uxarray.readthedocs.io/en/latest/index.html)\n", + "* [UXarray \"Why UXarray?\" Documentation](https://uxarray.readthedocs.io/en/latest/getting-started/overview.html)\n", + "* [UXarray Data Structures Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/data-structures.html)\n", + "* [UXarray Supported Models & Grid Formats Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/grid-formats.html)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Xarray](https://docs.xarray.dev/en/stable/) | Necessary | |\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----" + ] + }, + { + "cell_type": "markdown", + "id": "1b2069e5-78f9-4e3d-a2fd-276200d8e019", + "metadata": {}, + "source": [ + "## What is UXarray?" + ] + }, + { + "cell_type": "markdown", + "id": "589487e6-6e1e-4554-bddf-11ad00411ba4", + "metadata": {}, + "source": [ + "The “U” in UXarray stands for “Unstructured Grids”. UXarray extends upon and inherits from [Xarray](https://docs.xarray.dev/en/stable/) for providing the Earth system sciences community with analysis and visualization functionality that can operate directly on the native unstructured mesh data resulting from climate and global weather models and observations.\n", + "\n", + "UXarray is written around the [UGRID](http://ugrid-conventions.github.io/ugrid-conventions/) conventions to represent several different unstructured mesh types in an unified, [Climate and Forecast metadata convention (CF)](http://cfconventions.org/)-compliant format.\n", + "\n", + "Specifically, UXarray's core functionality supports horizontally unstructured grids with vertical levels as defined by the 2D Flexible Mesh Topology in the UGRID conventions, which can contain a mix of triangles, quadrilaterals, or other geometric faces." + ] + }, + { + "cell_type": "markdown", + "id": "dcd197af-7d93-49b2-90cf-ebd578caf82a", + "metadata": {}, + "source": [ + "## Why UXarray? " + ] + }, + { + "cell_type": "markdown", + "id": "f8fddc01-c109-4394-a720-67b1cda1a0f3", + "metadata": {}, + "source": [ + "UXarray can simplify working with unstructured grids because it:\n", + "\n", + "- Enables significant data analysis and visualization functionality to be executed directly on unstructured grids\n", + "\n", + "- Adheres to the UGRID specifications for compatibility across a variety of mesh formats\n", + "\n", + "- Provides a single interface for supporting a variety of unstructured grid formats including UGRID, MPAS, SCRIP, and Exodus (See the full list of [UXarray's supported models and grid formats](https://uxarray.readthedocs.io/en/latest/user-guide/grid-formats.html))\n", + "\n", + "- Inherits from Xarray, providing simplified data using familiar (Xarray-like) data structures and operations\n", + " \n", + "- Brings standardization to unstructured mesh support for climate data analysis and visualization\n", + "\n", + "- Builds on optimized data structures and algorithms for handling large and complex unstructured datasets\n", + "\n", + "- Supports enhanced interoperability and community collaboration" + ] + }, + { + "cell_type": "markdown", + "id": "9ef83bae-6cd3-4551-8e90-dae28d52c478", + "metadata": {}, + "source": [ + "## UXarray Design" + ] + }, + { + "cell_type": "markdown", + "id": "02d66394-cd52-46ba-bf2e-ab0aca83a31c", + "metadata": {}, + "source": [ + "UXarray is designed around three core data structures, `Grid`, `UxDataset`, and `UxDataArray`:\n", + "\n", + "* ``Grid`` provides unstructured grid awareness for the data analysis and visualization functionality implemented through `UxDataset` and `UxDataArray`. It houses grid-specific methods and topology variables.\n", + "\n", + "* ``UxDataset`` inherits from the ``xarray.Dataset`` class, providing much of the same functionality but extended to operate on Unstructured Grids through new and overloaded methods. Each ``UxDataset`` is linked to its own ``Grid`` object through the use of a class property (``UxDataset.uxgrid``) to provide a grid-aware implementation. An instance of ``UxDataset`` can be thought of as a collection of data variables, i.e. ``UxDataArray`` objects, that reside on a particular Unstructured Grid as defined in the ``uxgrid`` property.\n", + "\n", + "* ``UxDataArray`` similarly inherits from the ``xarray.DataArray`` class, providing single data variable-level functionality to operate on Unstructured Grids through new and overloaded methods. Similar to ``UxDataset``, it has a link to its own ``Grid`` object (``UxDataArray.uxgrid``). For isntance, if several ``UxDataArray`` variables exist in an ``UxDataset`` object, each of them will have a link to the same ``Grid`` object that proves all of them reside on the same unstructured grid topology." + ] + }, + { + "cell_type": "markdown", + "id": "7210be23-ce58-4bf9-b862-1d35b1ea08b1", + "metadata": {}, + "source": [ + "
\n", + "

Note:

\n", + " The following notebooks will detail each of these data structures; hence, we are keeping it short here in this section.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "defd539f-efe5-4966-bd55-3067c0b83ecb", + "metadata": {}, + "source": [ + "Given the introductions to the core UXarray data structures, let us have a look at the UXarray design diagram below:" + ] + }, + { + "cell_type": "markdown", + "id": "81ed8d9a-06d1-4446-a82d-dafdf97b758b", + "metadata": {}, + "source": [ + "\"Grid" + ] + }, + { + "cell_type": "markdown", + "id": "331a30b1-373b-4f40-af05-55a3a60f745d", + "metadata": {}, + "source": [ + "## UXarray's Core API and Unstructured Grids" + ] + }, + { + "cell_type": "markdown", + "id": "1af5c0eb-3976-4f34-b049-d6f775e5dfe9", + "metadata": {}, + "source": [ + "When working with unstructured grids, the grid topology definition is typically stored in a dedicated grid file, separately from any data file(s), i.e. the whole dataset contains a single grid definition file along with one or more data files. However, in other less common cases, the grid definition might be embedded into the data file(s). \n", + "\n", + "UXarray is able to handle both of these cases thanks to the core API for opening grid and data files (or other storage objects). The following notebooks in this chapter will cover the core API for these cases." + ] + }, + { + "cell_type": "markdown", + "id": "732c3182-5513-4bfb-a13f-ec7a655bef6d", + "metadata": {}, + "source": [ + "## What is next?\n", + "The next section will provide an introduction to the UXarray's ``Grid`` class and how to open standalone grid files." + ] + } + ], + "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.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/02-intro-to-uxarray/selection.ipynb b/notebooks/02-intro-to-uxarray/selection.ipynb new file mode 100644 index 00000000..21e40ac7 --- /dev/null +++ b/notebooks/02-intro-to-uxarray/selection.ipynb @@ -0,0 +1,410 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d5886a2c-ebd2-4e80-8aa7-b111d74c76fb", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# Spatial Selection Operators\n", + "\n", + "### In this tutorial, you'll learn:\n", + "\n", + "* Using UXarray to select specific regions from an unstructured grid\n", + "\n", + "\n", + "### Related Documentation\n", + "\n", + "* [UXarray Subsetting Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/subset.html)\n", + "* [UXarray Cross-Section Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/cross-sections.html)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "|-----------------------------------------------| --- | --- |\n", + "| [UXDataset & UxDataArray Notebook](uxds-uxda) | Necessary | |\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----\n" + ] + }, + { + "cell_type": "markdown", + "id": "ba50a82c-0c16-4704-bab5-13446f550d82", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "When working with unstructured grids, a region or zone rather than the entire global grid might be of interest for data analysis and visualization. Such spatial selection of the grid/data might help with not only analyzing/plotting a specific region/zone, but also reducing the data size and increasing the performance as well as allowing the entire plots to be effectively displayed on the screen.\n", + "\n", + "This notebook showcases how to spatially subset a grid and take a cross-section of the grid at a constant latitude and longitude." + ] + }, + { + "cell_type": "markdown", + "id": "95356e7a-bbba-4648-bbac-10d609855ce4", + "metadata": {}, + "source": [ + "## Subsetting\n", + "\n", + "UXarray provides functionality for subsetting the grid into a **bounding box**, **bounding circle**, or **K-nearest neighbors**. \n", + "\n", + "Before demonstrating them, let us load some global data first:" + ] + }, + { + "cell_type": "markdown", + "id": "6815426b-ab86-41c4-b04f-e7546c5e6e7e", + "metadata": {}, + "source": [ + "### Load Data" + ] + }, + { + "cell_type": "code", + "id": "b619a6a6-eaaa-4e5b-b15f-98502b8f4b28", + "metadata": { + "scrolled": true + }, + "source": [ + "# Import\n", + "import cartopy.crs as ccrs\n", + "import geoviews.feature as gf\n", + "import uxarray as ux\n", + "\n", + "grid_path = \"../../meshfiles/x1.655362.grid.nc\"\n", + "data_path = \"../../meshfiles/x1.655362.data.nc\"\n", + "\n", + "# Open dataset and grab a data variable of interest\n", + "uxds = ux.open_dataset(grid_path, data_path)\n", + "uxda = uxds[\"relhum_200hPa\"][0]" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "71695b1f-e0b6-4e6d-b6ff-3b0b927e4f0f", + "metadata": {}, + "source": [ + "### Plot The Global Data" + ] + }, + { + "cell_type": "markdown", + "id": "43ae12eb-92cb-4b55-9d49-14774dcefb7c", + "metadata": {}, + "source": [ + "
\n", + "

Note!

\n", + " The visualizations throughout this tutorial are only for demonstrating the results of the subsetting and cross-sections versus the global grid. Since the details of plotting with UXarray will be covered in the next chapter, we will not go over any details of the plots here.\n", + "
" + ] + }, + { + "cell_type": "code", + "id": "e623aac7-6177-497e-852b-31d7224906e6", + "metadata": {}, + "source": [ + "plot_opts = {\"width\": 700, \"height\": 350}\n", + "\n", + "features = gf.coastline(\n", + " projection=ccrs.PlateCarree(), line_width=0.4, scale=\"50m\"\n", + ") * gf.states(projection=ccrs.PlateCarree(), line_width=0.4, scale=\"50m\")\n", + "\n", + "clim = (uxda.values.min(), uxda.values.max())\n", + "\n", + "uxda.plot(\n", + " rasterize=True, periodic_elements=\"exclude\", title=\"Global Grid\", **plot_opts\n", + ") * features" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "f53848bf-7630-4b4e-979a-99c48628c464", + "metadata": {}, + "source": [ + "### Generate Various Subsets" + ] + }, + { + "cell_type": "code", + "id": "ead0c311-d3b7-41aa-8523-7260ce829d6f", + "metadata": {}, + "source": [ + "# Bounding box around Boulder, CO\n", + "ref_lon = -105.2705\n", + "ref_lat = 40.0150\n", + "ref_offset = 1\n", + "\n", + "lon_bounds = (ref_lon - ref_offset, ref_lon + ref_offset)\n", + "lat_bounds = (ref_lat - ref_offset, ref_lat + ref_offset)\n", + "\n", + "# Subset the global data variable and its grid using the bounding box\n", + "uxda_bbox = uxda.subset.bounding_box(lon_bounds, lat_bounds, element=\"nodes\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "2fcb49ce-4c9f-473a-9c93-8d0a0b42a7b1", + "metadata": {}, + "source": [ + "# Now use a bounding circle subsetting\n", + "ref_center = [ref_lon, ref_lat]\n", + "\n", + "uxda_bcircle = uxda.subset.bounding_circle(ref_center, ref_offset)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "1de8acc4-5b2f-4cdd-ad02-a6f4017cafa9", + "metadata": {}, + "source": [ + "# Now use a K-nearest neighbor subsetting\n", + "k_nn = 60\n", + "\n", + "uxda_nn = uxda.subset.nearest_neighbor(ref_center, k=k_nn, element=\"nodes\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "31b8019b-a965-49b8-9490-6481f50bddcd", + "metadata": {}, + "source": [ + "### Plot The Subsets" + ] + }, + { + "cell_type": "code", + "id": "88d7c474-c762-43d0-917e-b282f7109a0c", + "metadata": { + "scrolled": true + }, + "source": [ + "plot_bbox = (\n", + " uxda_bbox.plot(\n", + " rasterize=True,\n", + " periodic_elements=\"exclude\",\n", + " clim=clim,\n", + " title=\"Bounding Box Subset around Boulder, CO (Corner Node Query)\",\n", + " **plot_opts,\n", + " )\n", + " * features\n", + ")\n", + "\n", + "plot_bcircle = (\n", + " uxda_bcircle.plot(\n", + " rasterize=True,\n", + " periodic_elements=\"exclude\",\n", + " clim=clim,\n", + " title=\"Bounding Circle Subset around Boulder, CO (Corner Node Query)\",\n", + " **plot_opts,\n", + " )\n", + " * features\n", + ")\n", + "\n", + "plot_nn = (\n", + " uxda_nn.plot(\n", + " rasterize=True,\n", + " periodic_elements=\"exclude\",\n", + " clim=clim,\n", + " title=\"K-Nearest Neighbor Subset around Boulder, CO (Corner Node Query)\",\n", + " **plot_opts,\n", + " )\n", + " * features\n", + ")\n", + "\n", + "(plot_bbox + plot_bcircle + plot_nn).cols(1)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "125509b9-3b8e-46dc-8523-38ceeb01beee", + "metadata": {}, + "source": [ + "## Cross-sections\n", + "\n", + "Similarly, UXarray provides functionality for taking cross-sections of a grid/data at constant **latitudes** and **longitudes**. " + ] + }, + { + "cell_type": "markdown", + "id": "fc734a30-5fa9-4157-bda4-731a902c89ba", + "metadata": {}, + "source": [ + "### Load Data" + ] + }, + { + "cell_type": "code", + "id": "78b774d6-db3d-4d4a-aefa-1293a13e33a7", + "metadata": { + "scrolled": true + }, + "source": [ + "# Data paths\n", + "grid_path = \"../../meshfiles/outCSne30.grid.ug\"\n", + "data_path = \"../../meshfiles/outCSne30.data.nc\"\n", + "\n", + "# Open dataset and grab a data variable of interest\n", + "uxds = ux.open_dataset(grid_path, data_path)\n", + "uxda = uxds[\"psi\"]" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "193e9dce-55b0-4900-9312-14480a2e76ac", + "metadata": {}, + "source": [ + "### Plot The Global Data" + ] + }, + { + "cell_type": "code", + "id": "21a7e180-8afa-461a-a976-4a9370290b2e", + "metadata": {}, + "source": [ + "projection = ccrs.Robinson()\n", + "\n", + "features_2 = gf.coastline(\n", + " projection=projection, line_width=0.4, scale=\"50m\"\n", + ") * gf.states(projection=projection, line_width=0.4, scale=\"50m\")\n", + "\n", + "uxda.plot(\n", + " cmap=\"inferno\",\n", + " periodic_elements=\"split\",\n", + " projection=projection,\n", + " title=\"Global Plot\",\n", + ") * features_2" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "ded07a82-7a7a-4a55-8e74-9c1d71fe5154", + "metadata": {}, + "source": [ + "### Generate Cross-Sections" + ] + }, + { + "cell_type": "code", + "id": "e869e010-86f9-4eaa-b1f1-8568fb4907ed", + "metadata": {}, + "source": [ + "# Cross-section at the latitude of Boulder, CO\n", + "cross_lat = 40.0150\n", + "\n", + "uxda_cross_lat = uxda.cross_section.constant_latitude(cross_lat)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "9f629337-21f9-4448-a181-612c038fa92a", + "metadata": {}, + "source": [ + "# Now cross-section at the longitude of Boulder, CO\n", + "cross_lon = -105.2705\n", + "\n", + "uxda_cross_lon = uxda.cross_section.constant_longitude(cross_lon)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "8ef8af94-c6f4-468d-95ea-534d6130cfdb", + "metadata": {}, + "source": [ + "### Plot The Cross-Sections" + ] + }, + { + "cell_type": "code", + "id": "53e0f8dc-b51f-473e-b184-8ddc5203e7e9", + "metadata": {}, + "source": [ + "plot_cross_lat = (\n", + " uxda_cross_lat.plot(\n", + " rasterize=False,\n", + " backend=\"bokeh\",\n", + " cmap=\"inferno\",\n", + " projection=projection,\n", + " global_extent=True,\n", + " coastline=True,\n", + " title=f\"Cross-section at ({cross_lat}) degrees latitude around Boulder, CO\",\n", + " )\n", + " * features_2\n", + ")\n", + "\n", + "plot_cross_lon = (\n", + " uxda_cross_lon.plot(\n", + " rasterize=False,\n", + " backend=\"bokeh\",\n", + " cmap=\"inferno\",\n", + " projection=projection,\n", + " global_extent=True,\n", + " coastline=True,\n", + " title=f\"Cross-section at ({cross_lon}) degrees longitude around Boulder, CO\",\n", + " )\n", + " * features_2\n", + ")\n", + "\n", + "(plot_cross_lat + plot_cross_lon).cols(1)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "977a1608-cf7e-4342-967e-a088523e7630", + "metadata": {}, + "source": [ + "## What is next?\n", + "With this section, we have wrapped up this chapter, move on to the next chapter!" + ] + } + ], + "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.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/02-intro-to-uxarray/uxds-uxda.ipynb b/notebooks/02-intro-to-uxarray/uxds-uxda.ipynb new file mode 100644 index 00000000..7689699f --- /dev/null +++ b/notebooks/02-intro-to-uxarray/uxds-uxda.ipynb @@ -0,0 +1,4253 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "defae8d5978518cb", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# UXarray Dataset & DataArray\n", + "\n", + "### In this section, you'll learn:\n", + "\n", + "* What are `UxDataset` and `UxDataArray`?\n", + "* How do they relate to their Xarray counterparts?\n", + "\n", + "## Related Documentation\n", + "\n", + "* [UXarray Data Structures Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/data-structures.html)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "| --- | --- | --- |\n", + "| [Introduction to Xarray](https://foundations.projectpythia.org/core/xarray/xarray-intro.html) | Necessary | |\n", + "\n", + "**Time to learn**: 15 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "8b0f60ea-73c4-4f6f-849f-fb0642fcb78c", + "metadata": {}, + "source": [ + "## Overview" + ] + }, + { + "cell_type": "markdown", + "id": "e6539b10-cca3-4c93-8368-528b157b26a1", + "metadata": {}, + "source": [ + "As stated before in this chapter, ``UxDataset`` and ``UxDataArray`` are the UXarray classes that are inherited from Xarray counterparts and enable to perform analysis and visualization on unstructured mesh datasets. \n", + "\n", + "An instance of ``UxDataset`` can be thought of as a collection of data variables, i.e. ``UxDataArray`` objects, that reside on a particular unstructured grid as defined in the class property (``UxDataset.uxgrid`` or ``UxDataArray.uxgrid``).\n", + "\n", + "In other words, each of these data structures is linked to its own ``Grid`` object through ``uxgrid`` to enable grid-awareness, and for ``UxDataArray``s that belong to a specific ``UxDataset``, ``uxgrid`` of all of them points to the same ``Grid`` object. " + ] + }, + { + "cell_type": "markdown", + "id": "3a6e4836-6c49-4598-98c0-9750923e84c0", + "metadata": {}, + "source": [ + "## Unstructured Mesh Datasets" + ] + }, + { + "cell_type": "markdown", + "id": "20746384-c123-4282-8db2-d75e47f6cb25", + "metadata": {}, + "source": [ + "Since most of the unstructured grid datasets contain a single grid definition file along with one or more data files, UXarray's core API allows to open such datasets using those files. \n", + "\n", + "Let the following dataset have a single data file along with the grid definition: " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "409a3239-e51b-4353-8418-29dc1693a4dd", + "metadata": { + "ExecuteTime": { + "end_time": "2024-12-18T19:26:50.031729Z", + "start_time": "2024-12-18T19:26:50.029554Z" + } + }, + "outputs": [], + "source": [ + "grid_path = \"../../meshfiles/outCSne30.grid.ug\"\n", + "data_path = \"../../meshfiles/outCSne30.data.nc\"" + ] + }, + { + "cell_type": "markdown", + "id": "0cbfc5ef-d38e-42dd-a582-4c4259fe64ac", + "metadata": {}, + "source": [ + "## Loading a `UxDataset`" + ] + }, + { + "cell_type": "markdown", + "id": "29e59517-526f-434e-8647-bb8301118950", + "metadata": {}, + "source": [ + "To open a grid file with a **single** data file, the `uxarray.open_dataset()` method can be used. UXarray also provides `uxarray.open_mfdataset()` for opening multiple data files at once (refer to the [UXarray Opening Multipled Data Files Documentation](https://uxarray.readthedocs.io/en/latest/user-guide/data-structures.html#opening-multiple-data-files) for that), but this notebook will cover only the former." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d5560f80-09f1-4c5f-b542-0c7c52ba729b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.UxDataset> Size: 43kB\n",
+       "Dimensions:  (n_face: 5400)\n",
+       "Dimensions without coordinates: n_face\n",
+       "Data variables:\n",
+       "    psi      (n_face) float64 43kB ...
" + ], + "text/plain": [ + " Size: 43kB\n", + "Dimensions: (n_face: 5400)\n", + "Dimensions without coordinates: n_face\n", + "Data variables:\n", + " psi (n_face) float64 43kB ..." + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import uxarray as ux\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)\n", + "uxds" + ] + }, + { + "cell_type": "markdown", + "id": "aa440c7e-0cfd-4ff1-b72f-a694dddd8a25", + "metadata": {}, + "source": [ + "### Grid Accessor\n", + "\n", + "Let us first see the ``Grid``object this dataset object is linked to:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e08df73d-f3e1-463d-94f3-a98f4e8aaeff", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<uxarray.Grid>\n",
+       "Original Grid Type: UGRID\n",
+       "Grid Dimensions:\n",
+       "  * n_node: 5402\n",
+       "  * n_face: 5400\n",
+       "  * n_max_face_nodes: 4\n",
+       "  * n_nodes_per_face: (5400,)\n",
+       "Grid Coordinates (Spherical):\n",
+       "  * node_lon: (5402,)\n",
+       "  * node_lat: (5402,)\n",
+       "Grid Coordinates (Cartesian):\n",
+       "Grid Connectivity Variables:\n",
+       "  * face_node_connectivity: (5400, 4)\n",
+       "Grid Descriptor Variables:\n",
+       "  * n_nodes_per_face: (5400,)\n",
+       "
" + ], + "text/plain": [ + "\n", + "Original Grid Type: UGRID\n", + "Grid Dimensions:\n", + " * n_node: 5402\n", + " * n_face: 5400\n", + " * n_max_face_nodes: 4\n", + " * n_nodes_per_face: (5400,)\n", + "Grid Coordinates (Spherical):\n", + " * node_lon: (5402,)\n", + " * node_lat: (5402,)\n", + "Grid Coordinates (Cartesian):\n", + "Grid Connectivity Variables:\n", + " * face_node_connectivity: (5400, 4)\n", + "Grid Descriptor Variables:\n", + " * n_nodes_per_face: (5400,)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uxds.uxgrid" + ] + }, + { + "cell_type": "markdown", + "id": "a15b2d67-f30d-4c8f-b5c5-7f6ae82395fa", + "metadata": {}, + "source": [ + "## Accessing Data Variables (``UxDataArray``)\n", + "\n", + "Similar to the Xarray conterparts, an ``UxDataset`` can have multiple ``UxDataArray``s in it, which are for different data variables. A variable of interest can be accessed in the same way Xarray provides:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "79626e59-d4bc-4e6b-a108-001e017b6fd2", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.UxDataArray 'psi' (n_face: 5400)> Size: 43kB\n",
+       "[5400 values with dtype=float64]\n",
+       "Dimensions without coordinates: n_face
" + ], + "text/plain": [ + " Size: 43kB\n", + "[5400 values with dtype=float64]\n", + "Dimensions without coordinates: n_face" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uxds[\"psi\"]" + ] + }, + { + "cell_type": "markdown", + "id": "df0d0584-384f-45de-b63f-4c6803d15ed9", + "metadata": {}, + "source": [ + "### Grid Accessor\n", + "\n", + "The ``Grid``object of this data array is the same ``Grid`` object as ``uxds``'s above:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "06e0a099-5e71-4b42-afc8-6fa1856a8e5e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<uxarray.Grid>\n",
+       "Original Grid Type: UGRID\n",
+       "Grid Dimensions:\n",
+       "  * n_node: 5402\n",
+       "  * n_face: 5400\n",
+       "  * n_max_face_nodes: 4\n",
+       "  * n_nodes_per_face: (5400,)\n",
+       "Grid Coordinates (Spherical):\n",
+       "  * node_lon: (5402,)\n",
+       "  * node_lat: (5402,)\n",
+       "Grid Coordinates (Cartesian):\n",
+       "Grid Connectivity Variables:\n",
+       "  * face_node_connectivity: (5400, 4)\n",
+       "Grid Descriptor Variables:\n",
+       "  * n_nodes_per_face: (5400,)\n",
+       "
" + ], + "text/plain": [ + "\n", + "Original Grid Type: UGRID\n", + "Grid Dimensions:\n", + " * n_node: 5402\n", + " * n_face: 5400\n", + " * n_max_face_nodes: 4\n", + " * n_nodes_per_face: (5400,)\n", + "Grid Coordinates (Spherical):\n", + " * node_lon: (5402,)\n", + " * node_lat: (5402,)\n", + "Grid Coordinates (Cartesian):\n", + "Grid Connectivity Variables:\n", + " * face_node_connectivity: (5400, 4)\n", + "Grid Descriptor Variables:\n", + " * n_nodes_per_face: (5400,)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "uxda = uxds[\"psi\"]\n", + "uxda.uxgrid" + ] + }, + { + "cell_type": "markdown", + "id": "97f9b990-6a92-4de9-baf8-da7728ef67a5", + "metadata": {}, + "source": [ + "## Relationship to Xarray" + ] + }, + { + "cell_type": "markdown", + "id": "ffd122d2-513c-415d-97b9-5eb40db986af", + "metadata": {}, + "source": [ + "For users coming from an Xarray background, much of UXarray's design is familiar thanks to the UXarray's inheritance of the core data structures from Xarray's counterparts and employment of design choices such as accessors. Inheritance has been chosen for the UXarray design both to provide the users with a Xarray-like user experience and to use the built-in Xarray functionality whenever possible while overwrite the others for unstructured grid arithmetic.\n", + "\n", + "As a simple Xarray example that would replicate the above UXarray dataset opening and variable access with UXarray, please see the following code: " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "048279bd-3628-4478-9384-6807e3c24b65", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 30kB\n",
+       "Dimensions:  (lat: 45, lon: 80)\n",
+       "Coordinates:\n",
+       "  * lat      (lat) int64 360B -90 -86 -82 -78 -74 -70 -66 ... 66 70 74 78 82 86\n",
+       "  * lon      (lon) float64 640B -180.0 -175.5 -171.0 ... 166.5 171.0 175.5\n",
+       "Data variables:\n",
+       "    psi      (lat, lon) float64 29kB ...
" + ], + "text/plain": [ + " Size: 30kB\n", + "Dimensions: (lat: 45, lon: 80)\n", + "Coordinates:\n", + " * lat (lat) int64 360B -90 -86 -82 -78 -74 -70 -66 ... 66 70 74 78 82 86\n", + " * lon (lon) float64 640B -180.0 -175.5 -171.0 ... 166.5 171.0 175.5\n", + "Data variables:\n", + " psi (lat, lon) float64 29kB ..." + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Import\n", + "import xarray as xr\n", + "\n", + "# Structured grid data file\n", + "data_path_structured = \"../../meshfiles/outCSne30.structured.nc\"\n", + "\n", + "# Open dataset with Xarray\n", + "xrds = xr.open_dataset(data_path_structured)\n", + "xrds" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c641c1b3-c397-4856-91b5-95b4b3c9a3df", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray 'psi' (lat: 45, lon: 80)> Size: 29kB\n",
+       "[3600 values with dtype=float64]\n",
+       "Coordinates:\n",
+       "  * lat      (lat) int64 360B -90 -86 -82 -78 -74 -70 -66 ... 66 70 74 78 82 86\n",
+       "  * lon      (lon) float64 640B -180.0 -175.5 -171.0 ... 166.5 171.0 175.5\n",
+       "Attributes:\n",
+       "    regrid_method:  nearest_s2d
" + ], + "text/plain": [ + " Size: 29kB\n", + "[3600 values with dtype=float64]\n", + "Coordinates:\n", + " * lat (lat) int64 360B -90 -86 -82 -78 -74 -70 -66 ... 66 70 74 78 82 86\n", + " * lon (lon) float64 640B -180.0 -175.5 -171.0 ... 166.5 171.0 175.5\n", + "Attributes:\n", + " regrid_method: nearest_s2d" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "xrda = xrds[\"psi\"]\n", + "xrda" + ] + }, + { + "cell_type": "markdown", + "id": "833b03b9-f49e-4601-8127-7af61dc2b640", + "metadata": {}, + "source": [ + "
\n", + "

See also:

\n", + " For further information, please visit the \n", + " UXarray Inheritance from Xarray documentation\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "53694653-47d8-41c1-a7da-177af780b292", + "metadata": {}, + "source": [ + "## What is next?\n", + "The next section is the final section in this chapter that will provide information about how to operate spatial selection methods on ``UxDataArray``." + ] + } + ], + "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.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/02-methods/01-plotting-libraries.ipynb b/notebooks/02-methods/01-plotting-libraries.ipynb deleted file mode 100644 index 1a75748e..00000000 --- a/notebooks/02-methods/01-plotting-libraries.ipynb +++ /dev/null @@ -1,327 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Plotting Libraries\n", - "---\n", - "\n", - "We will introduce functionality from two visualization libraries and how they are being utilized by UXarray for unstructured grids visualization purposes. \n", - "\n", - "Before diving deep into these, looking into the current snapshot of the UXarray visualization design through a simple Unified Modelling Language (UML)-like diagram could be helpful to better understand UXarray's relation with such libraries. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Note:

\n", - " The following design diagram is actually provided in the Plotting\n", - " API section along with key takeaways about it. We highly recommend to check them out as well.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## UXarray Plotting API Design" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\"UXarray" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let us look into the visualization libraries that UXarray relies upon or provides an interface with." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## HoloViz" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[HoloViz](https://holoviz.org/) is a stack of tools (such as Holoviews, Datashader, Geoviews, SpatialPandas, hvPlot etc.) that provide high-level functionality to visualize even the very large datasets efficiently in Python. HoloViz packages are well-suited for unstructured grid visualization because:\n", - "\n", - "1. They provide rendering functionality for both vector geometries and rasterization, which will be detailed in the [next section](02-rendering-techniques). Such functionality is much needed for UXarray's grid topology and data visualization purposes.\n", - "2. Unlike Matplotlib, they support using the connectivity information that comes from the unstructured grids\n", - "3. They are designed to be scalable for even the largest datasets that'd be generated as result of kilometer-scale analyses" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Important!

\n", - " UXarray takes care of the vital tasks such as recognizing unstructured grids from various formats (such as UGRID,\n", - " MPAS, Scrip, Exodus, etc.) and representing them in a unified UGRID-like format, providing the data structures and\n", - " functionality necessary for convenient preprocessing of the grid, and wrapping up HoloViz packages' functionality to\n", - " enable unstructured grids-specialized, high-level visualization functions.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us look at the HoloViz packages that we utilize under the hood of UXarray to provide high-level, scalable visualization functions." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### HoloViews\n", - "\n", - "[Holoviews](https://holoviews.org/about.html) houses several elements (e.g. `Path()`, `Points()`) that we wrap up in the UXarray plotting accessors to enable visualization of grid geometries such as nodes and edges. Similarly, other elements of this package (e.g. `Polygons()`) are used by UXarray for various polygon vector visulization purposes." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Note:

\n", - " UXarray allows both Matplotlib and Bokeh backends to be chosen in visualization functions as they are provided by Holoviews (in addition to Plotly).\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Datashader" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[Datashader](https://datashader.org/about.html) is the graphics pipeline system of the HoloViz tool stack for creating meaningful representations of large datasets quickly and flexibly. We utilize Datashader rasterization methods, transfer functions, and other shading operators in our polyon rasterization code." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### GeoViews" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Actually, we do not wrap [GeoViews](https://geoviews.org/index.html) functionality in UXarray. Instead, we leave it up to the user's preferences to explore and visualize geographical, meteorological, and oceanographic datasets and features using the visulization outputs from our UXarray plotting functions. However, we love to enrich the plots in our documentation and examples with addition of a lot of Geoviews features such as ocean, land, coastlines, etc." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Spatialpandas" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Spatialpandas is a package that provides Pandas extension arrays for spatial/geometric operations. This package has an element called `GeoDataFrame`, which can be used directly by packages from the HoloViz stack such as hvPlot, Datashader, Holoviews, and Geoviews. Therefore, UXarray provides Grid conversions to `GeoDataFrame` to allow the user to perform visualizations directly in HoloViz packages rather than needing to use our UXarray plotting functions. \n", - "\n", - "Because this cookbook's main goal is to showcase UXarray's own visulization capabilities, we will not detail this conversion here but provide a link to a [UXarray user guide](https://uxarray.readthedocs.io/en/latest/user-guide/holoviz.html) that demonstrates this." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Matplotlib" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Matplotlib is the workhorse of Python visualization needs, for both general and geoscientific purposes. However, when it \n", - "comes to visualizing unstructured grids, Matplotlib's:\n", - "\n", - "1. Functionality is limited such that there is no way to use the connectivity information that comes with the unstructured grid \n", - "2. Scalability especially for kilometer-scale (e.g. individual storm-reoslving) resolutions is limited. \n", - "\n", - "We still support Matplotlib as a backend option in our visualization functionality, which will be covered in the next chapter. \n", - "\n", - "Moreover, just like conversion to `Spatialpandas.GeoDataFrame`, we provide conversion functions from UXarray to Matplotlib data structures such as collections, which can be utilized for visualizations directly in Matplotlib after the conversion." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Collections \n", - "Detailed information about Matplotlib's Collections API can be found [here](https://matplotlib.org/stable/api/collections_api.html). In Uxarray, conversions to `LineCollection` and `PolyCollection` are provided for visualizing Grid Geometries and data variables, respectively. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Warning!

\n", - " While these conversion functions have already been released, we have observed some issues with the resulting Matplotlib plots after the exceution of\n", - " these functions, and the bug-fixing of that is WIP. Hence, we don't have an officially released documentation/example about these functions yet.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Cartopy" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "Cartopy is originally a Python library for cartographic visualizations with Matplotlib; however, they provide a number of features such as `crs`, i.e. Coordinate Reference Systems (a.k.a. projections), that are significant for cartographic visualizations.\n", - "\n", - "While UXarray does not rely upon Cartopy, we support projections through our visualization functions with the help of `Cartopy.crs`. The use of such projections in the UXarray functions will be showcased in the next chapter; thus, let us stop at this point." - ] - } - ], - "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.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-plotting-with-uxarray/compare-xarray.ipynb b/notebooks/03-plotting-with-uxarray/compare-xarray.ipynb new file mode 100644 index 00000000..a1e74fb0 --- /dev/null +++ b/notebooks/03-plotting-with-uxarray/compare-xarray.ipynb @@ -0,0 +1,2234 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\"UXarray\n", + "\n", + "# Comparison to Xarray\n", + "\n", + "### In this tutorial, you'll learn about:\n", + "\n", + "* The differences and similarities between UXarray's and Xarray's plotting routines\n", + "* Using `hvPlot` with Xarray\n", + "\n", + "### Related Documentation \n", + "\n", + "* [UXarray Plotting User Guide](https://uxarray.readthedocs.io/en/latest/user-guide/plotting.html)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "|----------------------------------------------------------------| --- | --- |\n", + "| [Xarray](https://github.com/pydata/xarray) | Necessary | |\n", + "\n", + "**Time to learn**: 5 minutes\n", + "\n", + "-----\n", + "\n" + ], + "id": "c23c7abcaa8ff7e" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Introduction\n", + "\n", + "For users coming from an Xarray background, much of UXarray's design is familiar. This notebook showcases an example of transitioning a visualization of a structured grid using Xarray into a visualization of an unstructured grid using UXarray." + ], + "id": "743343ba3382fa62" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:01:54.186955Z", + "start_time": "2025-01-06T16:01:49.262839Z" + } + }, + "cell_type": "code", + "source": [ + "import cartopy.crs as ccrs\n", + "import matplotlib.pyplot as plt\n", + "import uxarray as ux\n", + "import xarray as xr" + ], + "id": "2955799360da72c0", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1002" + } + }, + "output_type": "display_data" + } + ], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Data\n", + "\n", + "We use two variations of the `outCSne30` grid for this example. One of them is the original unstructured cube sphere, with the other being a remapped structured version.\n", + "\n", + "### Xarray\n", + "\n" + ], + "id": "fa63b0546f47a95f" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:01:54.245991Z", + "start_time": "2025-01-06T16:01:54.196073Z" + } + }, + "cell_type": "code", + "source": [ + "base_path = \"../../meshfiles/\"\n", + "ds_path = base_path + \"outCSne30.structured.nc\"\n", + "xrds = xr.open_dataset(ds_path)\n", + "xrds" + ], + "id": "ec107de54b63dc12", + "outputs": [ + { + "data": { + "text/plain": [ + " Size: 30kB\n", + "Dimensions: (lat: 45, lon: 80)\n", + "Coordinates:\n", + " * lat (lat) int64 360B -90 -86 -82 -78 -74 -70 -66 ... 66 70 74 78 82 86\n", + " * lon (lon) float64 640B -180.0 -175.5 -171.0 ... 166.5 171.0 175.5\n", + "Data variables:\n", + " psi (lat, lon) float64 29kB ..." + ], + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 30kB\n",
+       "Dimensions:  (lat: 45, lon: 80)\n",
+       "Coordinates:\n",
+       "  * lat      (lat) int64 360B -90 -86 -82 -78 -74 -70 -66 ... 66 70 74 78 82 86\n",
+       "  * lon      (lon) float64 640B -180.0 -175.5 -171.0 ... 166.5 171.0 175.5\n",
+       "Data variables:\n",
+       "    psi      (lat, lon) float64 29kB ...
" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### UXarray", + "id": "9f1f5e2c3ac59196" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:01:54.554945Z", + "start_time": "2025-01-06T16:01:54.304835Z" + } + }, + "cell_type": "code", + "source": [ + "base_path = \"../../meshfiles/\"\n", + "grid_filename = base_path + \"outCSne30.grid.ug\"\n", + "data_filename = base_path + \"outCSne30.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_filename, data_filename)\n", + "uxds" + ], + "id": "3cc756d5ef606f9", + "outputs": [ + { + "data": { + "text/plain": [ + " Size: 43kB\n", + "Dimensions: (n_face: 5400)\n", + "Dimensions without coordinates: n_face\n", + "Data variables:\n", + " psi (n_face) float64 43kB ..." + ], + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.UxDataset> Size: 43kB\n",
+       "Dimensions:  (n_face: 5400)\n",
+       "Dimensions without coordinates: n_face\n",
+       "Data variables:\n",
+       "    psi      (n_face) float64 43kB ...
" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Visualization", + "id": "1d63ff5575ec892d" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Xarray", + "id": "4ac455addbf8eae1" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:02:12.366239Z", + "start_time": "2025-01-06T16:02:12.315756Z" + } + }, + "cell_type": "code", + "source": [ + "xrds[\"psi\"].plot(figsize=(12, 5), cmap=\"inferno\")" + ], + "id": "e7c42f8ae0133cf6", + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### UXarray", + "id": "13f56247aed4f12c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:02:13.001430Z", + "start_time": "2025-01-06T16:02:12.835029Z" + } + }, + "cell_type": "code", + "source": [ + "uxds[\"psi\"].plot(width=800, height=400, backend=\"matplotlib\", cmap=\"inferno\")" + ], + "id": "9ac0e713e185d875", + "outputs": [ + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + ":Image [x,y] (x_y psi)" + ] + }, + "execution_count": 8, + "metadata": { + "application/vnd.holoviews_exec.v0+json": {} + }, + "output_type": "execute_result" + } + ], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Using `hvPlot` to combine UXarray & Xarray Plots\n", + "\n", + "Since UXarray is written using `hvPlot`, we can visualize Xarray and UXarray plots together by using `hvplot.xarray`.\n", + "\n", + "
\n", + "

See also:

\n", + " To learn more about hvPlot and Xarray, please refer to the\n", + " hvPlot Documentation\n", + "
" + ], + "id": "2f20c7899617e191" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:03:53.481771Z", + "start_time": "2025-01-06T16:03:53.359230Z" + } + }, + "cell_type": "code", + "source": [ + "import holoviews as hv\n", + "import hvplot.xarray\n", + "\n", + "hv.extension(\"bokeh\")" + ], + "id": "ca6d5041bded9633", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1015" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = false;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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" + } + ], + "execution_count": 9 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T16:04:31.029518Z", + "start_time": "2025-01-06T16:04:28.735066Z" + } + }, + "cell_type": "code", + "source": [ + "(\n", + " xrds.hvplot(cmap=\"inferno\", title=\"Xarray with hvPlot\", width=800, height=400)\n", + " + uxds[\"psi\"].plot(\n", + " cmap=\"inferno\",\n", + " title=\"UXarray Plot\",\n", + " width=800,\n", + " height=400,\n", + " periodic_elements=\"split\",\n", + " )\n", + ").cols(1)" + ], + "id": "9c3f6b4a3f7bdde2", + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Layout\n", + " .Image.I :Image [lon,lat] (psi)\n", + " .Image.II :Image [x,y] (x_y psi)" + ] + }, + "execution_count": 14, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1595" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 14 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/03-plotting-with-uxarray/customization.ipynb b/notebooks/03-plotting-with-uxarray/customization.ipynb new file mode 100644 index 00000000..5a307e4b --- /dev/null +++ b/notebooks/03-plotting-with-uxarray/customization.ipynb @@ -0,0 +1,1520 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d315667-d148-4f56-84ca-bf1b5711a165", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# Customization & Interactivity\n", + "\n", + "### In this tutorial, you'll learn about:\n", + "\n", + "* Switching between Bokeh and Matplotlib backends for different rendering capabilities\n", + "* Setting up default plot parameters to maintain consistent styling across visualizations\n", + "* Using dynamic rasterization \n", + "* Leveraging Bokeh's interactive features for data exploration, including zooming, panning, and hover tools\n", + "\n", + "\n", + "### Related Documentation\n", + "\n", + "* [Plotting with Bokeh](https://hvplot.holoviz.org/user_guide/Plotting.html)\n", + "* [Plotting with Matplotlib](https://hvplot.holoviz.org/user_guide/Plotting_with_Matplotlib.html)\n", + "* [Customization](https://hvplot.holoviz.org/user_guide/Customization.html)\n", + "\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Introduction\n", + "\n", + "\n", + "An essential feature of UXarray is its seamless integration with hvPlot, a powerful plotting library that provides extensive customization and interactivity capabilities. This integration means that when you create visualizations using UXarray, you have access to most of hvPlot's features for tailoring your plots and making them interactive.\n" + ], + "id": "44e7efee6b47e07a" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:46.554797Z", + "start_time": "2025-01-08T22:51:40.163293Z" + } + }, + "cell_type": "code", + "source": [ + "import cartopy.crs as ccrs\n", + "import holoviews as hv\n", + "import uxarray as ux\n", + "from IPython.display import Image" + ], + "id": "7eb3b62a61d9303d", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1002" + } + }, + "output_type": "display_data" + } + ], + "execution_count": 1 + }, + { + "cell_type": "code", + "id": "3eb9ea47-4eb0-49c4-8953-45314afb3636", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:46.663146Z", + "start_time": "2025-01-08T22:51:46.563309Z" + } + }, + "source": [ + "grid_path = \"../../meshfiles/oQU480.grid.nc\"\n", + "data_path = \"../../meshfiles/oQU480.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)" + ], + "outputs": [], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Plot Parameters\n", + "\n", + "\n", + "For a comprehensive understanding of the available customization options, please refer to the [hvPlot Customization Guide](https://hvplot.holoviz.org/user_guide/Customization.html).\n", + "\n", + "Rather than duplicating the extensive documentation available in hvPlot's user guide, we encourage you to familiarize yourself with their customization options directly. This will provide you with the complete range of parameters available for tailoring your visualizations to meet your specific requirements." + ], + "id": "fd88dc7cac790ecc" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Selecting a Renderer\n", + "\n", + "One key advantage is the flexibility to choose your preferred plotting backend. While Bokeh serves as the default renderer, you can easily switch to Matplotlib by specifying the backend parameter in your plotting commands. This allows you to leverage the unique strengths of each renderer – Bokeh's interactive features or Matplotlib's publication-quality static plots.\n" + ], + "id": "5c1358ba4a509042" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:49.546061Z", + "start_time": "2025-01-08T22:51:46.731861Z" + } + }, + "cell_type": "code", + "source": [ + "hv.extension(\"bokeh\")\n", + "uxds[\"bottomDepth\"].plot()\n", + "\n", + "# can also do the following\n", + "# uxds[\"bottomDepth\"].plot(backend='bokeh')" + ], + "id": "aaeb78aa33df816d", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1013" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "\n", + "\n", + "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Image [x,y] (x_y bottomDepth)" + ] + }, + "execution_count": 3, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1015" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 3 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:50.542797Z", + "start_time": "2025-01-08T22:51:49.739176Z" + } + }, + "cell_type": "code", + "source": [ + "hv.extension(\"matplotlib\")\n", + "uxds[\"bottomDepth\"].plot()\n", + "\n", + "# can also do the following\n", + "# uxds[\"bottomDepth\"].plot(backend='matplotlib')" + ], + "id": "ca9c70f06ce1a4b7", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1159" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ], + "text/plain": [ + ":Image [x,y] (x_y bottomDepth)" + ] + }, + "execution_count": 5, + "metadata": { + "application/vnd.holoviews_exec.v0+json": {} + }, + "output_type": "execute_result" + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Setting Default Parameters\n", + "\n", + "When creating multiple visualizations, you may want to maintain consistent styling across your plots. HoloViews provides a convenient way to set default parameters through `hv.opts.defaults`, eliminating the need to repeatedly specify the same parameters for each plot.\n", + "\n", + "In the example below,we set default parameters for all Polygon plots using `hv.opts.defaults(hv.opts.Polygons())`. The specified parameters: width of 600 pixels, height of 300 pixels, and a default title, will automatically apply to any subsequent Polygon plots unless explicitly overridden.\n", + "\n", + "This approach is particularly useful when developing consistent visualizations for reports or presentations. Rather than manually setting dimensions and styling for each plot, you can establish these parameters once at the beginning of your notebook. This not only saves time but also ensures visual consistency throughout your analysis.\n", + "\n", + "Default parameters can be updated at any point in your notebook, giving you the flexibility to modify the styling of subsequent plots while maintaining the efficiency of centralized parameter management." + ], + "id": "c0035d7f129225b" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:50.647943Z", + "start_time": "2025-01-08T22:51:50.555836Z" + } + }, + "cell_type": "code", + "source": [ + "hv.extension(\"bokeh\")\n", + "hv.opts.defaults(hv.opts.Polygons(width=600, title=\"My Default Title\", height=300))" + ], + "id": "34e62c2e3f5ed655", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1161" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "\n", + "\n", + "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 6 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:50.753189Z", + "start_time": "2025-01-08T22:51:50.654728Z" + } + }, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot()" + ], + "id": "7b52c2f87002540b", + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Image [x,y] (x_y bottomDepth)" + ] + }, + "execution_count": 7, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1163" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 7 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Dynamic Rendering\n", + "\n", + "UXarray leverages hvPlot's dynamic rasterization feature through the `dynamic=True` parameter. This capability automatically adjusts the plot's resolution based on your current view, ensuring optimal performance without sacrificing data fidelity.\n", + "\n", + "As you zoom into specific regions of your plot, the visualization engine recalculates and re-rasterizes the data for your current viewport. This means you'll see increasing levels of detail as you examine smaller areas, revealing fine structures that might not be visible in the full-scale view. This adaptive approach maintains interactive performance while providing high-resolution details where they matter most.\n", + "\n", + "For example, when examining a global ocean mesh, you might start with a view of the entire grid. Upon zooming into a coastal region, the dynamic rasterization automatically refines the visualization, showing intricate mesh elements and local features that were previously aggregated in the broader view. This functionality is particularly valuable when working with multi-resolution meshes or datasets with varying spatial density.\n", + "\n", + "Since the rendered notebooks do not perform the re-rasterization, two GIFs have been provided with commented out code blocks. If you are working on this notebook locally, you can uncomment the code and explore the behavior interactively." + ], + "id": "4de519853ffcc42c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:50.972590Z", + "start_time": "2025-01-08T22:51:50.792681Z" + } + }, + "cell_type": "code", + "source": [ + "hv.extension(\"bokeh\")" + ], + "id": "54c587a609a2d6ab", + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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 const force = true;\n const py_version = '3.5.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\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 // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\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(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const 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 const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\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 const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const 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 (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\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 const js_urls = [\"https://cdn.holoviz.org/panel/1.5.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.3/dist/panel.min.js\", \"https://cdn.jsdelivr.net/npm/@holoviz/geoviews@1.13.0/dist/geoviews.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const 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 (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\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 // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\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 const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_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/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", + "application/javascript": "\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": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "" + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1235" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "\n", + "\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "\n", + "\n", + "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 8 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:50.994579Z", + "start_time": "2025-01-08T22:51:50.992880Z" + } + }, + "cell_type": "code", + "source": [ + "# uxds['bottomDepth'].plot()" + ], + "id": "d45a8ed9d625599e", + "outputs": [], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "![bokeh](https://i.postimg.cc/9MqxSYCn/bokeh-1.gif)" + ], + "id": "f250578d0f7fc66" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T22:51:50.998811Z", + "start_time": "2025-01-08T22:51:50.996584Z" + } + }, + "cell_type": "code", + "source": [ + "# uxds['bottomDepth'].plot(dynamic=True)" + ], + "id": "8829652f54efba05", + "outputs": [], + "execution_count": 10 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "![dynamic](https://i.postimg.cc/3JpTp2G9/dynamic.gif)", + "id": "de97b63e30c1bb31" + } + ], + "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 +} diff --git a/notebooks/03-plotting-with-uxarray/data-viz.ipynb b/notebooks/03-plotting-with-uxarray/data-viz.ipynb new file mode 100644 index 00000000..14b4d79b --- /dev/null +++ b/notebooks/03-plotting-with-uxarray/data-viz.ipynb @@ -0,0 +1,333 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\"UXarray\n", + "\n", + "# Data Visualization\n", + "\n", + "### In this tutorial, you'll learn about:\n", + "\n", + "* Working with data variables on unstructured grid elements including nodes, edges, and faces\n", + "* Polygon plotting techniques (vector & raster)\n", + "* Point plotting techniques (vector & raster)\n", + "* Handling periodic elements for 2D visualization\n", + "\n", + "\n", + "**Time to learn**: 15 minutes\n", + "\n", + "-----\n", + "\n" + ], + "id": "bb5bb1e53d861fd4" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Introduction\n", + "\n", + "\n", + "### Building on Grid Visualization\n", + "\n", + "Our exploration of unstructured grid visualization continues from the [previous section](grid-viz), where we examined geometric visualization using the `Grid` class. We now advance to the next critical aspect: visualizing data variables mapped to unstructured grid elements.\n", + "\n", + "### Understanding Data Element Mapping\n", + "\n", + "The visualization approach for unstructured grid data depends fundamentally on how data variables map to specific grid elements. Each variable may correspond to nodes, edges, or faces, and this mapping determines the most effective visualization strategy. This relationship between data and grid elements forms the foundation for selecting appropriate visualization techniques that accurately represent your data's spatial distribution and relationships." + ], + "id": "142fc358a6bd8cba" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import uxarray as ux" + ], + "id": "ac01a35ccd9136f1", + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "bc2d87ab434c9abc", + "metadata": {}, + "source": [ + "grid_path = \"../../meshfiles/oQU480.grid.nc\"\n", + "data_path = \"../../meshfiles/oQU480.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)\n", + "uxds[\"bottomDepth\"]" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "Our data variable above, `bottomDepth`, has a final dimension of `n_face`, meaning that it is mapped to the faces of our grid." + ], + "id": "14853d698e597985" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Plotting Accessor\n", + "\n", + "UXarray provides streamlined access to all visualization methods through the `UxDataArray.plot` accessor. This interface serves as the central entry point for data visualization, with each data array supporting a default visualization method when calling `UxDataArray.plot()`." + ], + "id": "c24e4d5338a787fb" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot()" + ], + "id": "8f11729c1ef589ae", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Polygons\n", + "\n", + "Face-centered data variables are visualized by default using raster polygon plots. In these plots, each face within the unstructured grid is represented as a distinct polygon. The polygons are shaded according to their corresponding data values.\n" + ], + "id": "eb56487f09d72dcc" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.polygons()" + ], + "id": "e18e61780a4bcc13", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Raster vs Vector Polygons\n", + "\n", + "By default, polygon rasterization is enabled (`rasterize=True`). This setting optimizes performance by converting vector polygons into a raster format when rendering. For high-resolution grids, maintaining rasterization is strongly recommended, as rendering individual vector polygons can significantly impact performance.\n", + "\n", + "Disabling rasterization (`rasterize=False`) switches to direct vector polygon rendering, which may be suitable for simpler visualizations where maintaining vector properties is essential." + ], + "id": "c79f32a58aca2383" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.polygons(rasterize=False)" + ], + "id": "3683a253cecf13d8", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Vector polygon plots maintain complete data fidelity regardless of zoom level, preserving both accuracy and visual quality throughout any magnification. This characteristic distinguishes them from rasterized alternatives, which may show pixelation upon close inspection.\n", + "\n", + "The following visualization demonstrates this advantage. When examining a magnified region of our grid, the vector-rendered polygons maintain their crisp definition and precise data representation.\n" + ], + "id": "78a557913d44d26c" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(\n", + " uxds[\"bottomDepth\"].plot.polygons(\n", + " rasterize=False, xlim=(-20, 0), ylim=(-5, 5), title=\"Vector Polygons\"\n", + " )\n", + " + uxds[\"bottomDepth\"].plot.polygons(\n", + " rasterize=True, xlim=(-20, 0), ylim=(-5, 5), title=\"Raster Polygons\"\n", + " )\n", + ").cols(1)" + ], + "id": "3170feafa0d80dd7", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Handling Periodic Elements\n", + "\n", + "\n", + "Visualizing unstructured grids on a spherical surface presents unique challenges, particularly when dealing with the antimeridian (180° longitude). This longitudinal line, where the Eastern and Western hemispheres meet, requires special consideration to ensure accurate 2D visualization of data that crosses this boundary.\n", + "\n", + "![Antimeridian Example](https://upload.wikimedia.org/wikipedia/commons/thumb/8/8d/Earth_map_with_180th_meridian.jpg/640px-Earth_map_with_180th_meridian.jpg)\n", + "\n", + "UXarray addresses this challenge through its `periodic_elements` parameter in polygon plotting functions. This parameter offers three approaches for managing faces that intersect with the antimeridian:\n", + "\n", + "`periodic_elements='exclude'` (Default)\n", + "Faces that cross the antimeridian are masked, removing them from the visualization. While this approach may omit some data, it offers optimal performance and is recommended for most use cases.\n", + "\n", + "`periodic_elements='split'`\n", + "Faces that cross the antimeridian are divided, ensuring complete data representation. However, this processing step impacts the initial plotting time. \n", + "\n", + "`periodic_elements='ignore'`\n", + "Faces that cross the antimeridian are not processed. \n", + "\n", + "\n", + "For optimal visualization performance, we recommend maintaining the default `periodic_elements='exclude'` setting unless complete data representation at the antimeridian is essential for your specific analysis.\n", + "\n", + "\n", + "\n" + ], + "id": "29ea37a88356169b" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(\n", + " uxds[\"bottomDepth\"].plot.polygons(\n", + " periodic_elements=\"exclude\", title=\"periodic_elements='exclude'\"\n", + " )\n", + " + uxds[\"bottomDepth\"].plot.polygons(\n", + " periodic_elements=\"split\", title=\"periodic_elements='split'\"\n", + " )\n", + ").cols(1)" + ], + "id": "b3dd8b49dd7e31f8", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "When working with data near or across the antimeridian (180° longitude), selecting an appropriate projection becomes crucial for accurate visualization. For comprehensive guidance on handling such scenarios, including recommended projections and geographic features, please refer to the [Geographic Projections & Features](geo) notebook.", + "id": "11f5cc8df330a6a1" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Polygon Plots for Node/Edge Data\n", + "\n", + "\n", + "Polygon plots require face-centered data variables, as each polygon must be rendered with a single value. This limitation presents a challenge when working with node-centered or edge-centered data, requiring a conversion process to map the data to faces.\n", + "\n", + "One effective approach is to perform topological averaging. This method calculates the mean value of adjacent nodes or edges and assigns the result to each face, creating a face-centered representation suitable for polygon visualization. While this transformation may introduce some averaging effects, it provides a practical solution for visualizing node and edge data within the constraints of polygon plotting.\n" + ], + "id": "582ebe3367af6c0d" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "grid_path = \"../../meshfiles/hex.grid.nc\"\n", + "data_path = \"../../meshfiles/hex.node.data.nc\"\n", + "\n", + "uxds_node_centered = ux.open_dataset(grid_path, data_path)" + ], + "id": "e8a55438a47681ea", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds_node_centered[\"random_data_node\"].topological_mean(destination=\"face\").plot()" + ], + "id": "d200de46ba97c821", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Points\n", + "\n", + "\n", + "Points provide an alternative visualization method that offers flexibility across different data mappings. This approach maps data values to specific coordinates, enabling visualization of node-centered, edge-centered, or face-centered data.\n", + "\n", + "The following visualization demonstrates this technique using face-centered data. Each point corresponds to a face center coordinate and displays its associated data value. This method maintains data fidelity while presenting the information in a discrete, coordinate-based format." + ], + "id": "698f98ef74101734" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.points()" + ], + "id": "eabad67e2d6fa17b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Rasterization\n", + "\n", + "As with polygon-based visualizations, point-based plots support rasterization through the `rasterize=True` parameter. This optimization technique converts vector-based points into a raster format during rendering." + ], + "id": "1e2ed9f38a153576" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.points(rasterize=True)" + ], + "id": "ddb3e5443f3c0722", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Point-based rasterization demonstrates different characteristics across varying data resolutions. For coarse-resolution data like our example, point-based visualization produces notably lower visual quality compared to polygon-based approaches. \n", + "\n", + "However, rasterized point plots emerge as a valuable tool for high-resolution grid visualization, offering significant performance advantages. For detailed information about leveraging this technique with high-resolution data, please refer to the [Visualizing High-Resolution Grids](high-res) section." + ], + "id": "5f9a0b768160cfb0" + } + ], + "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 +} diff --git a/notebooks/03-plotting-with-uxarray/geo.ipynb b/notebooks/03-plotting-with-uxarray/geo.ipynb new file mode 100644 index 00000000..22f8e50c --- /dev/null +++ b/notebooks/03-plotting-with-uxarray/geo.ipynb @@ -0,0 +1,6937 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0d315667-d148-4f56-84ca-bf1b5711a165", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# Geographic Projections & Features\n", + "\n", + "### In this tutorial, you'll learn about:\n", + "\n", + "* Selecting appropriate projections for your specific visualization needs\n", + "* Adding geographic features to provide context for your data\n", + "* Customizing projection parameters to optimize your visualizations\n", + "\n", + "Related Documentation\n", + "\n", + "* [hvPlot Geographic Options](https://hvplot.holoviz.org/user_guide/Customization.html#geographic-options)\n", + "\n", + "### Prerequisites\n", + "\n", + "| Concepts | Importance | Notes |\n", + "|----------------------------------------------------------|------------| --- |\n", + "| [Cartopy](https://scitools.org.uk/cartopy/docs/latest/) | Necessary | |\n", + "| [GeoViews](https://geoviews.org/) | Helpfull | |\n", + "\n", + "**Time to learn**: 5 minutes\n", + "\n", + "-----\n", + "\n", + "## Introduction\n", + "\n", + "When visualizing geospatial data on unstructured grids, choosing the right map projection is crucial for accurately representing your data. Geographic projections transform the three-dimensional surface of the Earth onto a two-dimensional plane, each offering different trade-offs between preserving area, distance, direction, or shape.\n", + "\n", + "UXarray leverages Cartopy's robust projection capabilities to provide flexible geographic visualization options. Through integration with hvPlot and GeoViews, UXarray allows you to easily switch between different projections and add geographic features like coastlines and borders to enhance your visualizations.\n", + "\n", + "We'll use practical examples to demonstrate these capabilities, focusing on common use cases in climate science, oceanography, and other geophysical applications where unstructured grids are prevalent.\n", + "\n" + ] + }, + { + "cell_type": "code", + "id": "3eb9ea47-4eb0-49c4-8953-45314afb3636", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T20:05:33.428345Z", + "start_time": "2025-01-08T20:05:33.410595Z" + } + }, + "source": [ + "import cartopy.crs as ccrs\n", + "import uxarray as ux" + ], + "outputs": [], + "execution_count": 7 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T20:05:33.759352Z", + "start_time": "2025-01-08T20:05:33.666434Z" + } + }, + "cell_type": "code", + "source": [ + "grid_path = \"../../meshfiles/oQU480.grid.nc\"\n", + "data_path = \"../../meshfiles/oQU480.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)" + ], + "id": "4a6e8f50f422cc4c", + "outputs": [], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Projections\n", + "\n", + "The geographic projection used for visualization can be specified through the `projection` parameter in UXarray's plotting methods. UXarray utilizes Cartopy's Coordinate Reference System (CRS) objects to define these projections.\n", + "\n", + "The simplest way to set a projection is to create a Cartopy CRS object and pass it to the plotting method. " + ], + "id": "8fd6db2da0250dc8" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T20:05:34.707310Z", + "start_time": "2025-01-08T20:05:34.465729Z" + } + }, + "cell_type": "code", + "source": [ + "projection = ccrs.Orthographic()\n", + "projection" + ], + "id": "d28e512b1397b381", + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "Name: unknown\n", + "Axis Info [cartesian]:\n", + "- E[east]: Easting (metre)\n", + "- N[north]: Northing (metre)\n", + "Area of Use:\n", + "- undefined\n", + "Coordinate Operation:\n", + "- name: unknown\n", + "- method: Orthographic\n", + "Datum: unknown\n", + "- Ellipsoid: unknown\n", + "- Prime Meridian: Greenwich" + ], + "text/html": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-01-08T14:05:34.649489\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.9.2, 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", + "
<cartopy.crs.Orthographic object at 0x000001CB15F7E570>
" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 9 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T20:05:38.572835Z", + "start_time": "2025-01-08T20:05:37.914878Z" + } + }, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.polygons(projection=projection)" + ], + "id": "635c3b24f12592b7", + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Image [Longitude,Latitude] (Longitude_Latitude bottomDepth)" + ] + }, + "execution_count": 10, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1219" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 10 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "When visualizing data concentrated in specific geographic regions, adjusting the projection's center point can significantly improve the visualization's clarity and accuracy. Many projections, including the Orthographic projection, provide `central_longitude` and `central_latitude` parameters for this purpose.\n", + "\n", + "This capability is particularly valuable when working with data near the antimeridian (180°/ longitude). For instance, data centered around the Pacific Ocean often crosses this boundary, which can cause visualization artifacts or split features across the edges of the plot. By setting `central_longitude=180`, you can shift the projection's center to properly display these regions:\n" + ], + "id": "e84640c5bdaed198" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T06:16:16.022042Z", + "start_time": "2025-01-08T06:16:15.736588Z" + } + }, + "cell_type": "code", + "source": [ + "projection = ccrs.Orthographic(central_longitude=-180)\n", + "projection" + ], + "id": "edb4e6fe87110b03", + "outputs": [ + { + "data": { + "text/plain": [ + "\n", + "Name: unknown\n", + "Axis Info [cartesian]:\n", + "- E[east]: Easting (metre)\n", + "- N[north]: Northing (metre)\n", + "Area of Use:\n", + "- undefined\n", + "Coordinate Operation:\n", + "- name: unknown\n", + "- method: Orthographic\n", + "Datum: unknown\n", + "- Ellipsoid: unknown\n", + "- Prime Meridian: Greenwich" + ], + "text/html": [ + "\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " 2025-01-08T00:16:15.946740\n", + " image/svg+xml\n", + " \n", + " \n", + " Matplotlib v3.9.2, 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", + "
<cartopy.crs.Orthographic object at 0x000001CB13D4BB60>
" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 5 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-08T06:16:20.874469Z", + "start_time": "2025-01-08T06:16:20.231620Z" + } + }, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.polygons(projection=projection)" + ], + "id": "53e10d6d99c2d68d", + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Image [Longitude,Latitude] (Longitude_Latitude bottomDepth)" + ] + }, + "execution_count": 6, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1116" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Features\n", + "\n", + "Geographic features are specified through the `features` parameter in your plotting command. You can add multiple features by providing them as a list. \n", + "\n", + "Available features include 'borders', 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'." + ], + "id": "ef100ea951e1279b" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T21:50:29.367545Z", + "start_time": "2025-01-06T21:50:28.185786Z" + } + }, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.polygons(\n", + " projection=projection, features=[\"borders\", \"coastline\"]\n", + ")" + ], + "id": "7cab99df47e55dfa", + "outputs": [ + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Overlay\n", + " .Image.I :Image [Longitude,Latitude] (Longitude_Latitude bottomDepth)\n", + " .Coastline.I :Feature [Longitude,Latitude]\n", + " .Borders.I :Feature [Longitude,Latitude]" + ] + }, + "execution_count": 9, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p1242" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 9 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The resolution of geographic features can be controlled by specifying a scale in the `features` parameter. This is done by passing a dictionary that maps each feature to its desired scale. There are three resolution levels: '10m' (highest detail), '50m' (medium detail), and '110m' (lowest detail). ", + "id": "2eabbd50c4c305ab" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-06T21:55:32.301076Z", + "start_time": "2025-01-06T21:55:21.552385Z" + } + }, + "cell_type": "code", + "source": [ + "uxds[\"bottomDepth\"].plot.polygons(\n", + " projection=projection, features={\"borders\": \"10m\", \"coastline\": \"10m\"}\n", + ")" + ], + "id": "cc41d0eec896bc02", + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/anaconda3/envs/uxarray/lib/python3.12/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/10m_cultural/ne_10m_admin_0_boundary_lines_land.zip\n", + " warnings.warn(f'Downloading: {url}', DownloadWarning)\n" + ] + }, + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "application/vnd.holoviews_exec.v0+json": "", + "text/plain": [ + ":Overlay\n", + " .Image.I :Image [Longitude,Latitude] (Longitude_Latitude bottomDepth)\n", + " .Coastline.I :Feature [Longitude,Latitude]\n", + " .Borders.I :Feature [Longitude,Latitude]" + ] + }, + "execution_count": 14, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "p7858" + } + }, + "output_type": "execute_result" + } + ], + "execution_count": 14 + } + ], + "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 +} diff --git a/notebooks/03-plotting-with-uxarray/grid-viz.ipynb b/notebooks/03-plotting-with-uxarray/grid-viz.ipynb new file mode 100644 index 00000000..a76484da --- /dev/null +++ b/notebooks/03-plotting-with-uxarray/grid-viz.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "580f49cb5fde2c24", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# Grid Visualization\n", + "\n", + "### In this section, you'll learn about:\n", + "* Plotting the edges of an unstructured grid\n", + "* Plotting the coordinates of an unstructured grid\n", + "\n", + "**Related Documentation**\n", + "\n", + "* [UXarray Plotting User Guide](https://uxarray.readthedocs.io/en/latest/user-guide/plotting.html)\n", + "\n", + "**Time to learn**: 10 minutes\n", + "\n", + "-----\n", + "\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Introduction\n", + "\n", + "Unstructured grids play a vital role in scientific computing and numerical simulations. UXarray's `Grid` data structure provides a robust foundation for working with these grids by managing essential variables like coordinates and connectivity information. Before conducting complex data analysis or running simulations, it's often crucial to visualize and verify the grid geometry.\n", + "\n", + "This visualization capability serves multiple purposes: It helps users understand the mesh topology, identify potential issues in grid generation, and verify boundary conditions. For example, when preparing to run a dynamical core simulation, scientists often examine the computational grid to ensure it meets their requirements for spatial resolution and element quality.\n", + "\n", + "To demonstrate these visualization techniques, we'll work with a straightforward example: An unstructured grid consisting of four hexagonal elements. " + ], + "id": "4037d881fcda3720" + }, + { + "cell_type": "code", + "id": "6bdf1449fbe884c4", + "metadata": {}, + "source": [ + "import uxarray as ux\n", + "\n", + "grid_path = \"../../meshfiles/hex.grid.nc\"\n", + "\n", + "uxgrid = ux.open_grid(grid_path)\n", + "uxgrid" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Plotting Accessor\n", + "\n", + "UXarray provides streamlined access to all visualization methods through the `Grid.plot` accessor. This interface serves as the central entry point for grid visualization, with each grid object supporting a default visualization method when calling `Grid.plot()`.\n" + ], + "id": "e7fadbf907530c66" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxgrid.plot(title=\"Default Plot Function\")" + ], + "id": "fd26dc3bc65484dd", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Accessing specific plotting functions is showcased below.", + "id": "b5cef0a110a390e5" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Visualizing Edges\n", + "\n", + "Edge visualization is a fundamental technique for understanding the unstructured grid topology. By plotting the edges between grid elements, we can examine the mesh structure, verify connectivity patterns, and assess the overall grid quality. The edge representation provides an intuitive view of how elements are arranged and connected.\n" + ], + "id": "c7a07184bc48fa47" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxgrid.plot.edges(title=\"Edge Plot\")" + ], + "id": "8fa4b5a83e176bc", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Visualizing Coordinates\n", + "\n", + "Unstructured grids incorporate three distinct types of geometric coordinates, each serving a specific purpose in defining the grid's spatial structure and properties.\n", + "\n", + "### Corner Nodes\n", + "\n", + "Corner nodes define the fundamental geometry of each grid element through their latitude and longitude coordinates, which are stored in the `Grid.node_lat` and `Grid.node_lon` variables. \n", + "\n" + ], + "id": "bb28fe23ce8bac55" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxgrid.plot.corner_nodes(title=\"Corner Nodes\") * uxgrid.plot()" + ], + "id": "7d9662c1e5dac8f7", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Face Centers\n", + "\n", + "Face centers represent the geometric centroid of each grid element, with their latitude and longitude coordinates stored in the `Grid.face_lat` and `Grid.face_lon` variables." + ], + "id": "366153db5c1e1f4f" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxgrid.plot.face_centers(title=\"Face Centers\") * uxgrid.plot()" + ], + "id": "6199ebccebd321e7", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Edge Centers\n", + "\n", + "Edge centers denote the midpoint of each grid boundary segment, with their spatial coordinates stored in the `Grid.edge_lat` and `Grid.edge_lon` variables. " + ], + "id": "af4b3dcc8e32e88e" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxgrid.plot.edge_centers(title=\"Face Centers\") * uxgrid.plot()" + ], + "id": "31a9075b5e5fa7a4", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Plotting Everything Together\n", + "\n", + "By combining the visualization of edges, face centers, corner nodes, and edge centers, we can generate a comprehensive representation of the unstructured grid's geometry. This allows us to example the complete spatial structure of our grid. \n" + ], + "id": "35505cab621918ed" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(\n", + " uxgrid.plot.edges(color=\"black\")\n", + " * uxgrid.plot.nodes(marker=\"o\", size=150).relabel(\"Corner Nodes\")\n", + " * uxgrid.plot.face_centers(marker=\"s\", size=150).relabel(\"Face Centers\")\n", + " * uxgrid.plot.edge_centers(marker=\"^\", size=150).relabel(\"Edge Centers\")\n", + ").opts(title=\"Grid Coordinates\", legend_position=\"top_right\")" + ], + "id": "d93c1ca0d2b3e2d0", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## ", + "id": "1f0c9ad6f9cedc55" + } + ], + "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 +} diff --git a/notebooks/03-plotting-with-uxarray/high-res.ipynb b/notebooks/03-plotting-with-uxarray/high-res.ipynb new file mode 100644 index 00000000..64bfb994 --- /dev/null +++ b/notebooks/03-plotting-with-uxarray/high-res.ipynb @@ -0,0 +1,297 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Visualizing High-Resolution Grids\n", + "\n", + "### In this tutorial, you'll learn about:\n", + "\n", + "* Techniques for visualizing high-resolution grids, including best practices for performance optimization and rendering\n", + "* The importance of rasterization in managing computational overhead when working with detailed global datasets\n", + "* Strategic use of subsetting functionality to enhance visualization performance for regional analyses\n", + "* Key differences between point-based and polygon-based visualization approaches at varying grid resolutions\n", + "* Effective use of map projections to improve spatial data representation and address point distribution challenges\n", + "* Control and optimization of visualization quality through pixel ratio parameters for both point and polygon plots\n", + "\n", + "**Time to learn**: 15 minutes\n", + "\n", + "-----" + ], + "id": "8dc00bbdf4ae22ed" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Introduction\n", + "\n", + "This notebook explores techniques for working with high-resolution unstructured grids, building upon the foundational concepts previously covered. While earlier examples focused on coarse-resolution grids to introduce core principles, many real-world applications require handling more detailed grid structures.\n", + "\n", + "For this demonstration, we utilize output from the 30km MPAS Atmosphere model, representing a significant increase in resolution compared to our previous examples. While this resolution allows us to effectively illustrate key visualization concepts, it's important to note that atmospheric and climate models often employ even higher resolutions. The concepts covered in this notebook are applicable to these higher resolutions. " + ], + "id": "7aa6e923186960ed" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import cartopy.crs as ccrs\n", + "import holoviews as hv\n", + "import uxarray as ux\n", + "\n", + "hv.extension(\"matplotlib\")" + ], + "id": "7f63f7bda1a0083f", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "grid_path = \"../../meshfiles/x1.655362.grid.nc\"\n", + "data_path = \"../../meshfiles/x1.655362.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)\n", + "uxds[\"relhum_200hPa\"]" + ], + "id": "4c48507c13b385b6", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Polygon Plots\n", + "\n", + "When working with high-resolution grids at global scales, performance optimization becomes crucial. As detailed in the [Data Visualization](data-viz) notebook, setting `rasterize=True` is strongly recommended for these scenarios. This parameter significantly improves rendering performance by converting vector-based polygons into raster images during visualization.\n", + "\n" + ], + "id": "9ab841b29b8c469" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "%%time\n", + "uxds[\"relhum_200hPa\"][0].plot.polygons(rasterize=True)" + ], + "id": "4dc9ab397752961b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The initial rendering of polygon plots requires significant processing time due to the conversion of each grid face into a Polygon object and its subsequent storage in a `GeoDataFrame`. However, this computational investment yields long-term benefits: once these geometries are processed and stored, they can be efficiently reused in subsequent visualizations. This caching mechanism ensures that follow-up polygon plots render substantially faster, making the initial processing time a worthwhile trade-off for improved ongoing performance. Below we can observe the faster rendering time after the initial execution.\n", + "id": "c60a488024a3caf3" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "%%time\n", + "res = uxds[\"relhum_200hPa\"][0].plot.polygons(rasterize=True)" + ], + "id": "9c7cf3194fd53d32", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Subsetting", + "id": "efd16248ef1cf775" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "To enhance performance when visualizing specific geographic regions, you can combine polygon plotting with subsetting functionality. This optimization strategy significantly reduces computational overhead by rendering only the polygons within your region of interest, rather than processing the entire grid. For example, when analyzing weather patterns over North America, subsetting the global grid to that continent will dramatically improve rendering speed and resource efficiency.\n", + "\n" + ], + "id": "58b208d4e0784876" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "relhum_subset = uxds[\"relhum_200hPa\"][0].subset.bounding_box(\n", + " lon_bounds=[-5, 5], lat_bounds=[-2.5, 2.5]\n", + ")\n", + "relhum_subset.plot.polygons(rasterize=True)" + ], + "id": "751339a98d8ed1fd", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "We can restrict our region even further to clearly see the original polygons.", + "id": "7bd1ed733d2d11ad" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "relhum_subset = uxds[\"relhum_200hPa\"][0].subset.bounding_box(\n", + " lon_bounds=[-0.5, 0.5], lat_bounds=[-0.25, 0.25]\n", + ")\n", + "relhum_subset.plot.polygons(rasterize=True)" + ], + "id": "c35ce4b6a72c4312", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Controlling Resolution\n", + "\n", + "The `pixel_ratio` parameter offers precise control over resolution in polygon plots. This parameter determines how closely the rasterized output approximates the original vector-based polygon representation.\n", + "\n", + "Setting a larger pixel ratio produces higher-fidelity visualizations that more accurately represent the underlying data structure and polygon geometries. These high-resolution outputs closely resemble traditional vector polygon plots, preserving intricate spatial relationships and boundaries.\n", + "\n", + "Conversely, specifying a smaller pixel ratio generates lower-resolution visualizations. While this approach may sacrifice some visual detail, it can significantly improve rendering performance for large datasets or when exact polygon boundaries are less critical for the analysis at hand.\n", + "\n", + "This resolution control mechanism enables you to balance visual accuracy with computational efficiency based on your specific visualization requirements and performance constraints." + ], + "id": "c3fa8585e9143701" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(\n", + " uxds[\"relhum_200hPa\"][0].plot.polygons(\n", + " rasterize=True, pixel_ratio=0.1, title=\"0.1 Pixel Ratio\"\n", + " )\n", + " + uxds[\"relhum_200hPa\"][0].plot.polygons(\n", + " rasterize=True, pixel_ratio=4.0, title=\"4.0 Pixel Ratio\"\n", + " )\n", + ").cols(1)" + ], + "id": "57e5ffbeac9810d5", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "## Point-Based Visualization Strategies\n", + "\n", + "Previous examination of rasterized point plots in the [Data Visualization](data-viz) section revealed their limitations for coarse-resolution grids. However, as grid resolution increases, this visualization method becomes increasingly advantageous, delivering superior rendering performance compared to polygon-based approaches while maintaining high visual quality.\n", + "\n", + "Our 30km global grid demonstration showcases this enhanced effectiveness. At this resolution, rasterized point plots achieve visual fidelity approaching that of traditional polygon plots, though with two important technical considerations to address during implementation:\n", + "\n", + "The visualization output exhibits missing values, denoted as `NaN` in the resulting display. Additionally, unprojected visualizations reveal an inherent characteristic of global coordinate systems: point density increases notably near the equator relative to the poles. This creates a distinctive variation in visual density—an expected artifact when representing spherical data in a planar format.\n", + "\n", + "\n", + "\n", + "\n" + ], + "id": "aa61ba5cf19e5748" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "%%time\n", + "uxds[\"relhum_200hPa\"][0].plot.points(rasterize=True)" + ], + "id": "aca476503fb5281b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Optimizing Point Distribution Using Map Projections\n", + "\n", + "Using appropriate map projections significantly enhances point distribution offering marked improvements over unprojected representations. Map projections help normalize the spatial distribution of data points, substantially reducing the clustering effects observed in standard latitude-longitude visualizations.\n", + "\n", + "For example, an Orthographic projection provides one effective approach, though various other projections can also help. While projections address many distribution challenges, some missing values may persist in the output. However, these remaining gaps typically represent a considerable improvement over unprojected versions, where equatorial clustering creates more pronounced visualization artifacts." + ], + "id": "a41a70cdc95d1eb6" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "%%time\n", + "uxds[\"relhum_200hPa\"][0].plot.points(rasterize=True, projection=ccrs.Orthographic())" + ], + "id": "549e63365b8fc93a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Controlling Resolution & Binning\n", + "\n", + "The `pixel_ratio` parameter provides precise control over the rasterization process by determining the size of individual raster pixels in your visualization. This parameter directly influences both the visual resolution and the data aggregation characteristics of your plot.\n", + "\n", + "When you specify a larger pixel ratio, the visualization creates smaller raster pixels. This higher-resolution approach means fewer data points are aggregated within each pixel, potentially leading to gaps in the visualization where pixels contain no data points. These empty pixels appear as `NaN` values in the final output.\n", + "\n", + "Conversely, setting a smaller pixel ratio creates larger pixels that capture and aggregate more data points. This approach typically produces a more continuous visualization by reducing the likelihood of empty pixels, though it may sacrifice some fine detail in the process.\n", + "\n", + "The pixel ratio effectively serves as a resolution control mechanism, allowing you to balance between granular detail and visual continuity based on your specific visualization requirements. This flexibility becomes particularly valuable when working with datasets of varying densities or when focusing on specific geographic regions that require different levels of detail." + ], + "id": "cf6c23dd793f0db6" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(\n", + " uxds[\"relhum_200hPa\"][0].plot.points(\n", + " rasterize=True,\n", + " projection=ccrs.Orthographic(),\n", + " pixel_ratio=0.5,\n", + " title=\"0.5 Pixel Ratio\",\n", + " )\n", + " + uxds[\"relhum_200hPa\"][0].plot.points(\n", + " rasterize=True,\n", + " projection=ccrs.Orthographic(),\n", + " pixel_ratio=3.0,\n", + " title=\"3.0 Pixel Ratio\",\n", + " )\n", + ").cols(1)" + ], + "id": "e138e378c8885b35", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/03-uxarray-vis/002-xarray-to-uxarray.ipynb b/notebooks/03-uxarray-vis/002-xarray-to-uxarray.ipynb deleted file mode 100644 index 8c3d2ca6..00000000 --- a/notebooks/03-uxarray-vis/002-xarray-to-uxarray.ipynb +++ /dev/null @@ -1,345 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "43c7b4fd0400a2cf", - "metadata": {}, - "source": [ - "# Comparison to Xarray\n", - "---" - ] - }, - { - "cell_type": "markdown", - "id": "d63bff006f925e79", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "For users coming from an Xarray background, much of UXarray's design is familiar. This notebook showcases an example of transitioning a Structured Grid Xarray workflow to UXarray." - ] - }, - { - "cell_type": "markdown", - "id": "7e11308e17c6c3e4", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Imports" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2af9b678e625324e", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import uxarray as ux\n", - "import xarray as xr" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a778749cfe150f46", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "fig_size = 400\n", - "plot_kwargs = {\"backend\": \"matplotlib\", \"aspect\": 2, \"fig_size\": fig_size}" - ] - }, - { - "cell_type": "markdown", - "id": "eb783270aa3cd61e", - "metadata": {}, - "source": [ - "## Data\n", - "\n", - "It is common practice to resample unstructured grids to a structured representation for many analysis workflows to utilize familiar and reliable tools. \n", - "\n", - "The datasets used in this example are meant to mimic this workflow, with a source Unstructured Grid and a Structured representation of that same grid." - ] - }, - { - "cell_type": "markdown", - "id": "86adf7bacc16426a", - "metadata": {}, - "source": [ - "### Structured" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76fbd63614250d5c", - "metadata": {}, - "outputs": [], - "source": [ - "base_path = \"../../meshfiles/\"\n", - "ds_path = base_path + \"outCSne30.structured.nc\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "195a6bb56f3884b2", - "metadata": {}, - "outputs": [], - "source": [ - "xrds = xr.open_dataset(ds_path)\n", - "xrds" - ] - }, - { - "cell_type": "markdown", - "id": "f1d48b216144c6ae", - "metadata": {}, - "source": [ - "### Unstructured" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b7b53a674ef8c160", - "metadata": {}, - "outputs": [], - "source": [ - "base_path = \"../../meshfiles/\"\n", - "grid_filename = base_path + \"outCSne30.grid.ug\"\n", - "data_filename = base_path + \"outCSne30.data.nc\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4b62b86021023dd9", - "metadata": {}, - "outputs": [], - "source": [ - "uxds = ux.open_dataset(grid_filename, data_filename)\n", - "uxds" - ] - }, - { - "cell_type": "markdown", - "id": "5a3fd6f861c7d783", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Example Workflows\n", - "Below are two simple visualization workflows that someone would run into\n", - "* Creating a single plot\n", - "* Creating a pair of plots (two different color maps are used to mimic different data)" - ] - }, - { - "cell_type": "markdown", - "id": "7eac8bf3-06c0-4c8c-95de-cd493f5d786f", - "metadata": {}, - "source": [ - "### Xarray" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e6aa862c57fe4da", - "metadata": {}, - "outputs": [], - "source": [ - "xrds[\"psi\"].plot(figsize=(12, 5), cmap=\"inferno\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "eb805e49ec1bb33c", - "metadata": {}, - "outputs": [], - "source": [ - "fig, axs = plt.subplots(nrows=2, figsize=(12, 10))\n", - "\n", - "xrds[\"psi\"].plot(cmap=\"inferno\", ax=axs[0])\n", - "xrds[\"psi\"].plot(cmap=\"cividis\", ax=axs[1])" - ] - }, - { - "cell_type": "markdown", - "id": "07709d9d-0cef-4738-8c34-cd9fd1b08d10", - "metadata": {}, - "source": [ - "### UXarray" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "acac06e7a32e65e", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"psi\"].plot(width=1000, height=500, cmap=\"inferno\")" - ] - }, - { - "cell_type": "markdown", - "id": "d8cc8b42-e627-4ba6-962c-bd31e704980e", - "metadata": {}, - "source": [ - "The default plotting method works great, but we can chose to set `exclude_antimeridian=False` to include the entire grid and generate a better looking plot. \n", - "\n", - "
\n", - "

See also:

\n", - " To learn more about this type of plotting functionality and supported parameters, please refer to the \n", - " Polygon Section\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "40c6faab6ef8483e", - "metadata": {}, - "outputs": [], - "source": [ - "uxds[\"psi\"].plot(width=1000, height=500, cmap=\"inferno\", exclude_antimeridian=False)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3d41ac5733a2d96e", - "metadata": {}, - "outputs": [], - "source": [ - "(\n", - " uxds[\"psi\"].plot(cmap=\"inferno\", exclude_antimeridian=False, **plot_kwargs)\n", - " + uxds[\"psi\"].plot(cmap=\"cividis\", exclude_antimeridian=False, **plot_kwargs)\n", - ").opts(fig_size=fig_size).cols(1)" - ] - }, - { - "cell_type": "markdown", - "id": "38ecc1cf67fdacfa", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Using `hvPlot` to conbine UXarray and Xarray Plots\n", - "\n", - "One of the primary drawbacks to UXarray's use of HoloViews for visualization is that there is no direct way to integrate plots generated with Xarray and UXarray together. This can be alleviated by using the `hvPlot` library, specifically `hvplot.xarray`, on Xarray's data structures. \n", - "\n", - "
\n", - "

See also:

\n", - " To learn more about hvPlot and xarray, please refer to the\n", - " hvPlot Documentation\n", - "
\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "932414b32ee3e2ea", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import holoviews as hv\n", - "import hvplot.xarray\n", - "\n", - "hv.extension(\"bokeh\")" - ] - }, - { - "cell_type": "markdown", - "id": "ae2fee146e9291ae", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "By using `xrds.hvplot()` as opposed to `xrds.plot()`, we can create a simple figure showcasing our Structured Grid figure from Xarray and Unstructured Grid figure from UXarray in a single plot. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "54e4a34ce6e94609", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "hv.extension(\"bokeh\")\n", - "(\n", - " xrds.hvplot(cmap=\"inferno\", title=\"Xarray with hvPlot\", width=1000, height=500)\n", - " + uxds[\"psi\"].plot(\n", - " cmap=\"inferno\",\n", - " title=\"UXarray Plot\",\n", - " exclude_antimeridian=False,\n", - " width=1000,\n", - " height=500,\n", - " )\n", - ").cols(1)" - ] - } - ], - "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.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/03-uxarray-vis/007-customization.ipynb b/notebooks/03-uxarray-vis/007-customization.ipynb deleted file mode 100644 index d2f4f1d8..00000000 --- a/notebooks/03-uxarray-vis/007-customization.ipynb +++ /dev/null @@ -1,43 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e2acf8c9c0b39643", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "# Customization\n", - "\n", - "
\n", - "

Note:

\n", - " This notebook is currently under development! Please refer to the Holoviews Documentation \n", - "
" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/03-uxarray-vis/01-plot-api.ipynb b/notebooks/03-uxarray-vis/01-plot-api.ipynb deleted file mode 100644 index f8733eb2..00000000 --- a/notebooks/03-uxarray-vis/01-plot-api.ipynb +++ /dev/null @@ -1,438 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Plotting API\n", - "---\n", - "\n", - "UXarray provides a feature-rich plotting API for visualizing unstructured grids, with or without data variables.\n", - "\n", - "This notebook introduces how to interface with the plotting methods through UXarray's core data structures and provides an introduction to methods that are covered in detail in the next sections." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "
\n", - "

See also:

\n", - " This notebook acts as an introduction into using the UXarray plotting API. Please refer to the following notebooks in this chapter for a detailed\n", - " overview of visualization techniques for different purposes (e.g. Grid Topology, Polygons)\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## UXarray Plotting API Design" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Before jumping into any code, let's take a look at a high-level snapshot of UXarray's API design from an Unifed Modeling Language (UML)-like standpoint.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\"UXarray\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Key takeaways from this design are that:\n", - "\n", - "- UXarray's unified grid representation (through the UGRID conventions) means that all visualization functionality is agnostic to the grid format initially provided by the user.\n", - "- Each Uxarray data structure (i.e. `Grid`, `UxDataset`, `UxDataArray`) has its own `.plot` accessor, which is used to call plotting routines.\n", - "- The visualization functionality through these `.plot` accessors use HoloViz packages' plotting functions, wrapping them in a way to exploit all the information that comes from unstructured grids (e.g. connectivity) and provide our unstructured grids-specific functions in the most convenient way for the user.\n", - "- `Grid` additionally provides conversion functions that generate `SpatialPandas.GeoDataFrame` as well as `Matplotlib.PolyCollection` and `Matplotlib.LineCollection` data structures to be visualized in HoloViz packages and Matplotlib, respectively, at the user's own convenience.\n", - "\n", - "Now, we can see the API in action on a sample dataset." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Imports" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import uxarray as ux" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Data\n", - "\n", - "The grid and data files used in this notebook are from 480km MPAS Ocean model output.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "base_path = \"../../meshfiles/\"\n", - "grid_filename = base_path + \"oQU480.grid.nc\"\n", - "data_filename = base_path + \"oQU480.data.nc\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid = ux.open_grid(grid_filename)\n", - "grid" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds = ux.open_dataset(grid_filename, data_filename)\n", - "uxds" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Grid\n", - "\n", - "Since a `Grid` instance only contains information about the topology of an unstructured grid (a.k.a. no data variables), the visualizations generated from the `Grid` class only showcase the coordinates and connectivity.\n", - "\n", - "By default, the `Grid.plot` method renders the borders of each of the faces." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid.plot(title=\"Default Grid Plot\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The UXarray plotting API is written with HoloViews, with the default backend used for generating plots being `bokeh`. This means that by default, all plots are enabled with interactive features such as panning and zooming. In addition to `bokeh`, UXarray also supports the `matplotlib` backend.\n", - "\n", - "For the remainder of this notebook, we will use the `matplotlib` backend to generate static plots." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid.plot(\n", - " title=\"Default Grid Plot with Matplotlib\",\n", - " backend=\"matplotlib\",\n", - " aspect=2,\n", - " fig_size=500,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "\n", - "\n", - "You can call specific plotting routines through the `plot` accessor" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid.plot.nodes(title=\"Grid Node Plot\", backend=\"matplotlib\", aspect=2, fig_size=500)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "Since each `UxDataset` and `UxDataArray` is always linked to a `Grid` instance through the `uxgrid` attribute, all of these grid-specific visualizations are accessible by using that attribute.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds.uxgrid.plot(\n", - " title=\"Grid plot through uxgrid attribute\",\n", - " backend=\"matplotlib\",\n", - " aspect=2,\n", - " fig_size=500,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## UxDataArray Plotting" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The default plotting method is a great starting point for visualizations. It selects what visualization method to use based on the grid element that the data is mapped to (nodes, edges, faces) and the number of elements in the mesh. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot(\n", - " title=\"Default UxDataArray Plot\", backend=\"matplotlib\", aspect=2, fig_size=500\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "We can also call other plotting methods through the `plot` accessor, as was the case with the `Grid` class.\n", - "\n", - "For example, if we wanted to rasterize the polygons and exclude the ones that cross the antimeridian, it would look something like the following." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.polygons(\n", - " exclude_antimeridian=False,\n", - " title=\"Vector Polygon Plot\",\n", - " backend=\"matplotlib\",\n", - " aspect=2,\n", - " fig_size=500,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## UxDataset Plotting\n", - "\n", - "As of the most recent release, UXarray does not support plotting functionality through a `ux.UxDataset`. For instance, if the following commented out code was executed, it would throw an exception.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# uxds.plot()" - ] - } - ], - "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.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-uxarray-vis/02-grid-topology.ipynb b/notebooks/03-uxarray-vis/02-grid-topology.ipynb deleted file mode 100644 index 8647ef39..00000000 --- a/notebooks/03-uxarray-vis/02-grid-topology.ipynb +++ /dev/null @@ -1,422 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Grid Topology" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "No matter what analysis operations you are performing on the data, visualization of the geometric elements of an unstructured grid (i.e. nodes, edges) without any data mapped to them can always be useful for a number of reasons, including but not limited to understanding the mesh topology and diagnosing patterns or issues with or prior to data analysis (e.g. analyzing the mesh of a dynamical core prior to running a simulation).\n", - "\n", - "UXarray provides convenient functionality to visualize these elements of the `Grid` object. Below we will introduce those functions, but let us do the initial setup first:" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This setup handles the package imports and data loading." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Imports" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import cartopy.crs as ccrs\n", - "import geoviews.feature as gf\n", - "import holoviews as hv\n", - "import uxarray as ux" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Read in the data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "# File paths\n", - "file_dir = \"../../meshfiles/\"\n", - "\n", - "# We use 480km dataset in this example but there is also 120km dataset\n", - "# in the file directory that can be further explored\n", - "grid_filename = \"oQU480.grid.nc\"\n", - "data_filename = \"oQU480.data.nc\"\n", - "\n", - "# A standalone grid can be opened for immediate visualization\n", - "ux_grid = ux.open_grid(file_dir + grid_filename)\n", - "\n", - "# Visualization through a `UxDataset`'s associated grid, `uxds.uxgrid` is also possible.\n", - "uxds = ux.open_dataset(file_dir + grid_filename, file_dir + data_filename)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Important!

\n", - " How to plot through `Grid`, `UxDataset`, or `UxDataArray`?\n", - "
\n", - "
\n", - " As the above `ux_grid` and `uxds` creation suggests, you may either have a `UxDataset` (or similarly \n", - " `UxDataArray`), or a standalone `Grid` object. Visualization of the geometric elements of an \n", - " unstructured grid is possible in all of these cases as follows (Through `uxgrid` accessor when it is \n", - " a `UxDataset` or `UxDataArray`):\n", - "
\n", - " `uxarray.Grid.plot.plotting_function()`
\n", - " `uxarray.UxDataset.uxgrid.plot.plotting_function()`
\n", - " `uxarray.UxDataArray.uxgrid.plot.plotting_function()`
\n", - " We will demonstrate plotting functions using the standalone `ux_grid` thorughout this notebook.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting Edges" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Plotting the edges of an Unstructured Grid gives us an immediate idea of what the actual mesh looks like since connected edges construct the faces of the grid. Because of this, edge visualization is considered as the default method for Grid topology visualization purposes, and the default plotting method `uxarray.Grid.plot()`, provides an edge plot as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_grid.plot(\n", - " title=\"Default Grid Plot Method\",\n", - " width=700,\n", - " height=350,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Tip:

\n", - " See also `ux_grid.plot.mesh()` and `ux_grid.plot.edges()` that would both create the same plot.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Tip:

\n", - " Try `xlim` and `ylim` args with the above plot to plot a region of interest only, e.g. `xlim=(-170, -50), ylim=(10, 80)`.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Using `exclude_antimeridian`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The default grid `plot()` method as well as `mesh()` and `edges()` can take in an optional argument called `exclude_antimeridian`, which generates the visualization with or without correcting polygons that cross at the antimeridian (+- 180 longitude):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_grid.plot.mesh(\n", - " exclude_antimeridian=True,\n", - " title=\"Mesh - Exclude Antimeridian Polygons\",\n", - " width=700,\n", - " height=350,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Choosing a backend" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since all of our plotting methods are built around the Holoviews package, you can select between Matplotlib and Bokeh backends if desired (Bokeh is the default and is suggested). Let us have a quick look at a Matplotlib result as well:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_grid.plot.edges(\n", - " backend=\"matplotlib\", title=\"Edges - Matplotlib Backend\", fig_size=300, aspect=2\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting Nodes" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The nodes (a.k.a. vertices in some unstructured mesh models) construct the corner points of faces (a.k.a. cells) in unstructured grids, hence they can also give an idea about the mesh geometry when visualized. Nodes can be visualized easily with UXarray as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_grid.plot.nodes(size=1, title=\"Nodes\", width=700, height=350)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Tip:

\n", - " See also `ux_grid.plot.node_coords()` that would create the same plot.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting Face and Edge Centers" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In some cases, one might even want to visualize the center coordinates of the grid faces and edges. UXarray has functions to perform such visualizations as points:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_grid.plot.face_centers(size=1, title=\"Face Centers\", width=700, height=350)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Tip:

\n", - " See also `ux_grid.plot.face_coords()` that would create the same plot.\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_grid.plot.edge_centers(size=1, title=\"Edge Centers\", width=700, height=350)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

Tip:

\n", - " See also `ux_grid.plot.edge_coords()` that would create the same plot.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Putting All Together" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A more meaningful visualization case could be bringing all of these plotting functions together in a single figure (This time, let's use the `uxds` object to analyze its grid geometry):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "(\n", - " uxds.uxgrid.plot.edges(color=\"Black\", line_dash=\"dashed\", line_width=1)\n", - " * uxds.uxgrid.plot.nodes(color=\"Red\", size=1)\n", - " * uxds.uxgrid.plot.face_centers(color=\"Blue\", size=1)\n", - " * uxds.uxgrid.plot.edge_centers(color=\"Green\", size=1)\n", - ").opts(\n", - " title=\"Node, Edge, & Face Coordinates\",\n", - " xlim=(-170, -50),\n", - " ylim=(10, 80),\n", - " width=700,\n", - " height=350,\n", - ")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-uxarray-vis/03-polygons.ipynb b/notebooks/03-uxarray-vis/03-polygons.ipynb deleted file mode 100644 index 5e23b5a7..00000000 --- a/notebooks/03-uxarray-vis/03-polygons.ipynb +++ /dev/null @@ -1,659 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Polygons\n", - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "Unstructured Grids are often used in the Dynamical Cores of climate models because of their ability to represent complex geometries effectively. However, the way unstructured grids are represented makes it difficult to produce quick visualizations.\n", - "\n", - "Previously, we have discussed that data variables are typically mapped to the elements that compose unstructured grids (nodes, edges, and faces). When data is mapped to the faces, each face is shaded with the value of the data variable. We can treat these faces as Polygons on a plane for visualization.\n", - "\n", - "This notebook showcases UXarray's Polygon Visualization methods." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import uxarray as ux" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "fig_size = 400\n", - "plot_kwargs = {\"backend\": \"matplotlib\", \"aspect\": 2, \"fig_size\": fig_size}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "file_dir = \"../../meshfiles/\"\n", - "grid_filename_mpas = file_dir + \"oQU480.grid.nc\"\n", - "data_filename_mpas = file_dir + \"oQU480.data.nc\"\n", - "uxds = ux.open_dataset(grid_filename_mpas, data_filename_mpas)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Vector Polygon Plots\n", - "\n", - "The `UxDataArray.plot.polygons()` method produces a Vector Polygon plot, where each face is represent as a polygon. \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.polygons(**plot_kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Interactive Example\n", - "\n", - "In the plot below, take some time to zoom and pan around the plot to observe some of the defining characteristics of a Vector Polygon Plot:\n", - "* Hovering over a polygon (face) displays the value mapped to it, which is the same as the original value stored in our `UxDataArray`\n", - "* There is no drop in quality when zooming in\n", - "* Each polygon is bounded by edges that are drawn as lines\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.polygons(\n", - " backend=\"bokeh\", width=1000, height=500, tools=[\"hover\"]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Data Fidelity\n", - "\n", - "The defining characteristic of vector polygon plots is that there is no loss of data fidelity (i.e. the accuracy and/or quality) when zooming in. \n", - "\n", - "Zooming into a region of our grid highlights this, which can be seen in the plot below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.polygons(xlim=(-20, 0), ylim=(-5, 5), **plot_kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Rasterized Polygon Plots\n", - "\n", - "The `UxDataArray.plot.rasterize(method='polygon')` function produces a Rasterized Polygon plot. Instead of rendering each polygon exactly, they are rasterized into cells using Datashader internally. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.rasterize(method=\"polygon\", **plot_kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Interactive Example\n", - "\n", - "In the plot below, take some time to zoom and pan around to observe some of the defining characters of a Raster Polygon Plot and compare it to the Vector Polygon Plot\n", - "* Zooming in exposes the fact that each polygon is approximately rendered, with jagged edges around the boundaries of each cell\n", - "* Hovering over a polygon (face) still displays the original data values, even though the boundaries are not explicit like the vector example\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.rasterize(\n", - " method=\"polygon\", backend=\"bokeh\", width=1000, height=500, tools=[\"hover\"]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Data Fidelity" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If we restrict our viewing area to the same constraints as the zoomed in vector plot, we begin to see the effects of rasterization. Since rasterization approximates each polygon by binning the data values onto a fixed-size grid, our polygons appear jagged and no longer have clear boundaries like the vector plot. \n", - "\n", - "This means the effective resolution is much lower when zoomed in." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "uxds[\"bottomDepth\"].plot.rasterize(\n", - " method=\"polygon\", xlim=(-20, 0), ylim=(-5, 5), **plot_kwargs\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "However, a higher level of data fidelity can be achieved by specifying a `pixel_ratio`, which controls the size of the binning used for rasterization. \n", - "* A high pixel ratio increases the number of bins by making each bin smaller, leading to a higher effective resolution and a better approximation of the vector polygon plot\n", - "* A low pixel ratio decreases the number of bins by making each bin larger, leading to a lower effective resolution with jagged and coarse approximations of each polygon" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "(\n", - " uxds[\"bottomDepth\"].plot.rasterize(\n", - " xlim=(-20, 0),\n", - " ylim=(-5, 5),\n", - " pixel_ratio=0.5,\n", - " title=\"0.5 (low) Pixel Ratio\",\n", - " **plot_kwargs\n", - " )\n", - " + uxds[\"bottomDepth\"].plot.rasterize(\n", - " xlim=(-20, 0),\n", - " ylim=(-5, 5),\n", - " pixel_ratio=8.0,\n", - " title=\"8.0 (high) Pixel Ratio\",\n", - " **plot_kwargs\n", - " )\n", - ").opts(fig_size=fig_size / 1.5).cols(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Polygons Around Antimeridian\n", - "\n", - "When attempting to visualize unstructured meshes that reside on a sphere, it's necessary to consider the behavior of geometric elements near or on the Antimeridian. Elements that exist on or cross the antimeridian need to be corrected to properly visualize them. UXarray uses the antimeridian package to split faces along the antimeridian. More details can be found in their [documentation](https://antimeridian.readthedocs.io/en/stable/)." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "\"Antimeridian\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Opting to Include Antimeridian Polygons\n", - "\n", - "To include and correct antimeridian polygons, we can set `exclude_antimeridian=False`. \n", - "\n", - "The following plots are zoomed in to a section along the antimeridian. We can see that our plot is split exactly across the antimeridian." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "(\n", - " uxds[\"bottomDepth\"].plot.polygons(\n", - " xlim=(-185, -175),\n", - " ylim=(-5, 5),\n", - " exclude_antimeridian=False,\n", - " title=\"Left of Antimeridian\",\n", - " **plot_kwargs\n", - " )\n", - " + uxds[\"bottomDepth\"].plot.polygons(\n", - " xlim=(175, 185),\n", - " ylim=(-5, 5),\n", - " exclude_antimeridian=False,\n", - " title=\"Right of Antimeridian\",\n", - " **plot_kwargs\n", - " )\n", - ").opts(fig_size=fig_size / 1.5).cols(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Opting to Exclude Antimeridian Polygons\n", - "\n", - "To exclude antimeridian polygons, we can set `exclude_antimeridian=True`. \n", - "\n", - "In the following plots, we can see that the polygons that were corrected above are now missing. Since there is a relatively high overhead involved in detecting and correcting each antimeridian polygon, this option opts to exclude them from the overall visualuzation workflow, which leads to a significant performance increase at higher resolutions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "(\n", - " uxds[\"bottomDepth\"].plot.polygons(\n", - " xlim=(-185, -175),\n", - " ylim=(-5, 5),\n", - " exclude_antimeridian=True,\n", - " title=\"Left of Antimeridian\",\n", - " **plot_kwargs\n", - " )\n", - " + uxds[\"bottomDepth\"].plot.polygons(\n", - " xlim=(175, 185),\n", - " ylim=(-5, 5),\n", - " exclude_antimeridian=True,\n", - " title=\"Right of Antimeridian\",\n", - " **plot_kwargs\n", - " )\n", - ").opts(fig_size=fig_size / 1.5).cols(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Projection Support\n", - "\n", - "As of the most recent UXarray release, geographic projections and transformations are not supported by UXarray's polygon plotting functionality. However, we can still utilize UXarray paired with HoloViz tools to generate these types of plots with a bit of extra effort." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import cartopy.crs as ccrs\n", - "import holoviews as hv\n", - "import hvplot.pandas\n", - "\n", - "hv.extension(\"matplotlib\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Conversion to a SpatialPandas GeoDataFrame\n", - "\n", - "In order to support visualization with the popular HoloViz stack of libraries (hvPlot, HoloViews, Datashader, etc.), UXarray provides methods for converting Grid and UxDataArray objects into a SpatialPandas GeoDataFrame, which can be used for visualizing the polygons that represent each grid, in addition to data variables." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "gdf = uxds[\"bottomDepth\"].to_geodataframe()\n", - "gdf" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "
\n", - "

See also:

\n", - " For more infromation about UXarray and the conversion to a GeoDataFrame, please refer to the\n", - " UXarray Usage Examples \n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Using `hvPlot`\n", - "\n", - "Once we have a `spatialpandas.GeoDataFrame`, we can use the `.hvplot` accessor to generate Polygon plots with projections and geographic features. Below are a few examples:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "projection = ccrs.Robinson()\n", - "\n", - "gdf.hvplot.polygons(\n", - " rasterize=True,\n", - " c=\"bottomDepth\",\n", - " cmap=\"Blues\",\n", - " projection=projection,\n", - " height=600,\n", - " title=\"Robinson Projection\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "projection = ccrs.Robinson(central_longitude=-180)\n", - "\n", - "gdf.hvplot.polygons(\n", - " rasterize=True,\n", - " c=\"bottomDepth\",\n", - " cmap=\"Blues\",\n", - " projection=projection,\n", - " height=600,\n", - " title=\"Robinson Projection (centered about Antimeridian)\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import geoviews.feature as gf\n", - "\n", - "projection = ccrs.Robinson(central_longitude=-180)\n", - "\n", - "gdf.hvplot.polygons(\n", - " rasterize=True,\n", - " c=\"bottomDepth\",\n", - " cmap=\"Blues\",\n", - " projection=projection,\n", - " height=600,\n", - " title=\"Robinson Projection (Continents and Borders)\",\n", - ") * gf.coastline() * gf.borders()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "projection = ccrs.Orthographic(central_latitude=90)\n", - "\n", - "gdf.hvplot.polygons(\n", - " rasterize=True,\n", - " c=\"bottomDepth\",\n", - " cmap=\"Blues\",\n", - " projection=projection,\n", - " height=400,\n", - " title=\"Orthographic Projection over North Pole\",\n", - ") * gf.coastline() * gf.borders()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Takeaways\n", - "\n", - "Polygon plotting is UXarray's flagship visualization method, which provides a high level of data fidelity by utilizing the unstructured grid's connectivity information.\n", - "\n", - "\n", - "**When should I use vector or raster plots?**\n", - "* Rasterization should be used for most applications, especially when working with moderate to large grids. Also consider using an appropriate pixel ratio value to get a higher effective resolution\n", - "* Vector plots should be used when working with a small grid, typically under 10,000 elements because of the high overhead needed to render each polygon directly\n", - "\n", - "**When should I set `exclude_antimeridian=False`?**\n", - "* It is suggested to almost always keep `exclude_antimeridian=True` (the default option) since it leads to a very large performance increase\n", - "* For small grids or when excluding the polygons would lead to artifacts, it is recommend to set it to False\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-uxarray-vis/04-points.ipynb b/notebooks/03-uxarray-vis/04-points.ipynb deleted file mode 100644 index 44a55a43..00000000 --- a/notebooks/03-uxarray-vis/04-points.ipynb +++ /dev/null @@ -1,1017 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Points\n", - "---\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The primary unstructured grid elements (nodes, edges, faces) each have corresponding latitude-longer coordinates that represent some aspect of the element, such as the center of each face. These coordinates can be transformed into points and shaded with a data variable for visualization.\n", - "\n", - "This notebook showcases how to visualize data variables as points." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 107, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "import cartopy.crs as ccrs\n", - "import geoviews.feature as gf\n", - "import uxarray as ux\n", - "from holoviews import opts\n", - "\n", - "file_dir = \"../../meshfiles/\"" - ] - }, - { - "cell_type": "code", - "execution_count": 108, - "metadata": {}, - "outputs": [], - "source": [ - "fig_size = 400\n", - "plot_kwargs = {\"backend\": \"bokeh\", \"width\": 900, \"height\": 450}\n", - "ortho_plot_kwargs = {\"backend\": \"bokeh\", \"width\": 700, \"height\": 700}" - ] - }, - { - "cell_type": "code", - "execution_count": 109, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid_filename_120 = file_dir + \"oQU120.grid.nc\"\n", - "data_filename_120 = file_dir + \"oQU120.data.nc\"\n", - "uxds_120km = ux.open_dataset(grid_filename_120, data_filename_120)" - ] - }, - { - "cell_type": "code", - "execution_count": 110, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [ - "grid_filename_480 = file_dir + \"oQU480.grid.nc\"\n", - "data_filename_480 = file_dir + \"oQU480.data.nc\"\n", - "uxds_480km = ux.open_dataset(grid_filename_480, data_filename_480)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Representing Coordinates as Points\n", - "\n", - "For 2D visualizations, we can shade pairs of longitude and latitude coordinates as Points. UXarray supports three types of latitude-longitude (i.e spherical) coordinates:\n", - "\n", - "**`node_lon` and `node_lat`**:\n", - "* Longitudes and Latitudes of the corner nodes of each face\n", - "* Used for node-centered data variables\n", - "\n", - "**`edge_lon` and `edge_lat`**:\n", - "* Longitudes and Latitudes of the centers of each edge\n", - "* Can be derived from `node_lon` and `node_lat` internally by using Cartesian averaging\n", - "* Used for edge-centered data variables\n", - "\n", - "**`face_lon` and `face_lat`**:\n", - "* Longitudes and Latitudes of the centers of each face\n", - "* Can be derived from `node_lon` and `node_lat` internally by using Cartesian averaging\n", - "* Used for face-centered data variables\n", - "\n", - "In the previous notebook, we discussed how Polygon plots can be used for visualizing face-centered data. An alternate visualization of the same data variable can be achieved by shading Points instead of Polygons, which will be showcased in this notebook." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Vector Point Plots" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The `UxDataArray.plot.points()` method produces a Vector Point plot \n", - "\n", - "\n", - "We can plot each shaded data point using the latitude and longitude of either the nodes, edge centers, or face centers. Since ``bottomDepth`` is a face-centered variable, it is plotted using the node coordinates (i.e. ``node_lon`` and ``node_lat``)" - ] - }, - { - "cell_type": "code", - "execution_count": 111, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Points [x,y] (z)" - ] - }, - "execution_count": 111, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p10800" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "uxds_480km[\"bottomDepth\"].plot.points(**plot_kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The size of each point can be increased or decreased by specifying the `size` parameter" - ] - }, - { - "cell_type": "code", - "execution_count": 112, - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Points [x,y] (z)" - ] - }, - "execution_count": 112, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p10957" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "uxds_480km[\"bottomDepth\"].plot.points(size=5, **plot_kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "You may also select what projection to transform the coordinates into using the ``projection`` argument." - ] - }, - { - "cell_type": "code", - "execution_count": 113, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Points [x,y] (z)" - ] - }, - "execution_count": 113, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p11114" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "uxds_480km[\"bottomDepth\"].plot.points(\n", - " projection=ccrs.Orthographic(), size=5, **ortho_plot_kwargs\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Rasterized Point Plots\n", - "Instead of plotting the geometry of each point directly, we can rasterize our set of points on a fixed-size grid. \n" - ] - }, - { - "cell_type": "code", - "execution_count": 117, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Layout\n", - " .Image.I :Image [lon,lat] (lon_lat var)\n", - " .Image.II :Image [lon,lat] (lon_lat var)" - ] - }, - "execution_count": 117, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p12127" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "(\n", - " uxds_480km[\"bottomDepth\"].plot.rasterize(\n", - " method=\"point\", width=900, height=400, title=\"480km Grid\"\n", - " )\n", - " + uxds_120km[\"bottomDepth\"].plot.rasterize(\n", - " method=\"point\", width=900, height=400, title=\"120km Grid\"\n", - " )\n", - ").cols(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Pixel Ratio" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "As the resolution of our grid increases, observe how there is a much higher density of points, to the point where we can start to observe the trends of the data variable. \n", - "\n", - "As was the case with the Polygon raster, we can specify a `pixel_ratio` value to control the bin sized used for rasterization. To summarize:\n", - "\n", - "* A high pixel ratio increases the number of bins by making each bin smaller, meaning that on average less points fall into each bin, leading to a sparsity\n", - "* A low pixel ratio decreases the number of bins by making each bin larger, meaning that on average more points fall into each bin, decreasing the sparsity\n" - ] - }, - { - "cell_type": "code", - "execution_count": 118, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Layout\n", - " .Image.I :Image [lon,lat] (lon_lat var)\n", - " .Image.II :Image [lon,lat] (lon_lat var)" - ] - }, - "execution_count": 118, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p12476" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "(\n", - " uxds_120km[\"bottomDepth\"].plot.rasterize(\n", - " method=\"point\", pixel_ratio=0.40, title=\"0.4 Pixel Ratio\", **plot_kwargs\n", - " )\n", - " + (\n", - " uxds_120km[\"bottomDepth\"].plot.rasterize(\n", - " method=\"point\", pixel_ratio=2.0, title=\"2.0 Pixel Ratio\", **plot_kwargs\n", - " )\n", - " )\n", - ").cols(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "We can see that the 0.4 Pixel Ratio looks significantly better than the default example, while the 2.0 Pixel Ratio looks much sparser. \n", - "\n", - "However, there is still a lower density of points near the poles because of the way our data is projected. We can re-project the data to more evenly distribute the points " - ] - }, - { - "cell_type": "code", - "execution_count": 119, - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Image [lon,lat] (lon_lat var)" - ] - }, - "execution_count": 119, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p12825" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "uxds_120km[\"bottomDepth\"].plot.rasterize(\n", - " method=\"point\", projection=ccrs.Sinusoidal(), pixel_ratio=0.4, **plot_kwargs\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### High-Resolution Example" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The grids used in this notebook thus far have had resolutions of 480km and 120km. These resolutions are not sufficient for reaching high levels of data fidelity, since there is simply not enough points for the rasterization to look convincing.\n", - "\n", - "The following figures were rendered using UXarray on a 3.75km mesh, which had approximately 42 million faces and 84 million nodes. This means that there are 42 million points available for a face-centered data variable and 84 million points for a node-centered data variable. \n", - "\n", - "\n", - "![global plot](../images/plots/point_raster_global_no_proj.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The issue at the poles is still present, even with millions of points. It is still suggested to use a projection to better distribute the points." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "![global plot](../images/plots/point_raster_orthographic.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "
\n", - "

See also:

\n", - " A broader discussion about visualization at higher resolutions, including the pros and cons of Point Rasterization, is discussed in the Visualization at Scale notebook.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Takeaways\n", - "\n", - "Point plotting is a quick way to obtain visualizations of data variables by using the coordinates of the corner nodes, edge centers, or face centers to generate plots. \n", - "\n", - "**When should I use vector or raster plots?**\n", - "* Raster plots, when paired with an appropriate pixel ratio and projection, can produce quick and convincing plots comparable to Polygon plots at a fraction of the execution time\n", - "* Vector plots should be used sparingly, only when plotting the exact point is relevant, such as overlaying points onto another plot\n", - "\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-uxarray-vis/05-triangles.ipynb b/notebooks/03-uxarray-vis/05-triangles.ipynb deleted file mode 100644 index a0e8f81f..00000000 --- a/notebooks/03-uxarray-vis/05-triangles.ipynb +++ /dev/null @@ -1,102 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Triangles" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "
\n", - "

Note:

\n", - " This notebook is currently under development! Please refer to the Interactive HoloViz Visualization notebook in the Advanced Visualization Cookbook for an example of visualizing triangular meshes. \n", - "
" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-uxarray-vis/06-performance.ipynb b/notebooks/03-uxarray-vis/06-performance.ipynb deleted file mode 100644 index c28d5291..00000000 --- a/notebooks/03-uxarray-vis/06-performance.ipynb +++ /dev/null @@ -1,524 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Visualization at Scale\n", - "---" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "When working with large datasets, it's crucial to consider the performance of each visualization method. This notebook investigates the performance and data fidelity of the previously covered methods using high-resolution grids.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Benchmarked Dataset" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The dataset used for the timings and examples in this notebook were provided curtesy of the [DYAMOND Initiative](https://www.google.com/search?q=dyamond&oq=dyamond&gs_lcrp=EgZjaHJvbWUyCQgAEEUYORiABDIGCAEQIxgnMgcIAhAuGIAEMgkIAxAuGAoYgAQyCQgEEAAYChiABDIHCAUQLhiABDIHCAYQLhiABDIJCAcQABgKGIAEMgcICBAAGIAEMg8ICRAAGAoYgwEYsQMYgATSAQc5NjRqMGo0qAIAsAIA&sourceid=chrome&ie=UTF-8).\n", - "\n", - "There are four datasets, each from the same experiment, but with different grid resolutions. The table below summarizes the scale of these datasets:\n", - "\n", - "| Element / Resolution | 30km | 15km | 7.5km | 3.75km |\n", - "|----------------------|-----------|-----------|------------|------------|\n", - "| Faces | 655,362 | 2,621,442 | 10,485,762 | 41,943,042 |\n", - "| Nodes | 1,310,720 | 5,242,880 | 20,971,520 | 83,886,080 |\n", - "\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Data Processing Timings\n", - "\n", - "Timings were taken on a single [NCAR Derecho Node](https://arc.ucar.edu/knowledge_base/74317833). All results are in seconds. " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Initial Run" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "| Visualization Method / Grid Resolution | 30km | 15km | 7.5km | 3.75km |\n", - "|-----------------------------------------|-------------|-------------|------------|-------------|\n", - "| Polygon Raster (Including Antimeridian) | 28.5 | 122.2 | 463 | 1990 |\n", - "| Polygon Raster (Excluding Antimeridian) | 1.69 (0.23) | 5.96 (0.09) | 23.1 (0.52) | 93 (1.01) |\n", - "| Point Raster | 0.13 (0.03) | 0.16 (0.01) | 0.35 (0.00) | 1.08 (0.07) |" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "We can see that Point Rasters are quickest, averaging about 86 times faster than Polygon Rasters (Excluding Antimeridian Polygons).\n", - "\n", - "Both polygon methods scale linearly with an increase in resolution. A doubling in resolution leads to about a 4x increase in the number of polygons (a.k.a polygons), which is also observed in the timings.\n", - "\n", - "Including antimeridian polygons leads to about a 20x slowdown across all resolutions, so it's suggested to keep `exclude_antimeridian=True` when working with larger datasets." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Subsequent Runs" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "| Visualization Method / Grid Resolution | 30km | 15km | 7.5km | 3.75km |\n", - "|-----------------------------------------|-------------|-------------|-------------|--------------|\n", - "| Polygon Raster (Including Antimeridian) | 0.31 (0.00) | 1.32 (0.31) | 3.85 (0.06) | 14.36 (0.13) |\n", - "| Polygon Raster (Excluding Antimeridian) | 0.30 (0.00) | 1.02 (0.36) | 3.46 (0.01) | 13.60 (0.08) |\n", - "| Point Raster | 0.13 (0.03) | 0.16 (0.01) | 0.35 (0.00) | 1.08 (0.07) |\n", - "\n", - "For subsequent runs (i.e. we have already run one plotting instance, which computes and caches the necessary data structures), performance for both Polygon methods is essentially identical.\n", - "\n", - "There is no caching currently implemented for Point Rasters, so the performance for each run is consistent with the initial run.\n", - "\n", - "
\n", - "

Important!

\n", - " The timings above benchmark the data processing time (i.e. total time needed to transform an unstructured grid into a format that is able to be\n", - " rendered). Actual visualization times will vary depending on choices in parameters.\n", - "

\n", - " A detailed benchmark of total visualization times (i.e. data processing and rendering to screen) will be added to this notebook in the future.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "## Polygon vs Point Rasters\n", - "\n", - "Both the Polygon and Point notebooks showed off how these elements could be rasterizer." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Global\n", - "\n", - "For certain visualization workflows, one may only be interested in observing the global trends of a data variable.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "```Python\n", - "uxds['relhum_200hPa'][0].plot.rasterize(method='polygon', \n", - " width=1000, height=500, exclude_antimeridian=True, \n", - " clim=clim, \n", - " title=\"Global Polygon Raster\")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "![Polygon Raster](../images/plots/global_polygon_raster.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "```Python\n", - "uxds['relhum_200hPa'][0].plot.rasterize(method='point', \n", - " width=1000, \n", - " height=500, \n", - " clim=clim, \n", - " title=\"Global Point Raster\")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "\n", - "![Point Raster](../images/plots/global_point_raster.png)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "We can see that both the Polygon and Point Rasters capture the global trend of our data variable." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "### Regional\n", - "\n", - "However, it's also common to zoom into a region of interested (i.e. Continental United States, Europe, etc.) to observe how a data variable acts on these more refined regions." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "```Python\n", - "uxds['relhum_200hPa'][0].plot.rasterize(method='polygon', \n", - " width=1000, \n", - " height=500, \n", - " exclude_antimeridian=True, \n", - " dynamic=False, \n", - " xlim=(-68, -60), \n", - " ylim=(-71, -66), \n", - " clim=clim, \n", - " title=\"Regional Polygon Raster\")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "![Polygon Raster](../images/plots/regional_polygon_raster.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "```Python\n", - "uxds['relhum_200hPa'][0].plot.rasterize(method='point', \n", - " width=1000, \n", - " height=500, \n", - " dynamic=False, \n", - " xlim=(-68, -60), \n", - " ylim=(-71, -66), \n", - " clim=clim, \n", - " title=\"Regional Point Raster\")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "![Point Raster](../images/plots/regional_point_raster.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "Without specifying any additional parameters, both the Polygon and Point rasters look identical.\n", - "\n", - "However, setting the parameter ``dynamic=True``, which dynamically performs the rasterization operations as we zoom and pan across a plot, we can start to see the differences between both types of plots.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "```Python\n", - "uxds['relhum_200hPa'][0].plot.rasterize(method='polygon', \n", - " width=1000, \n", - " height=500, \n", - " exclude_antimeridian=True, \n", - " dynamic=True, xlim=(-68, -60), \n", - " ylim=(-71, -66), \n", - " clim=clim, \n", - " title=\"Regional Polygon Raster (Dynamic)\")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "![Polygon Raster](../images/plots/regional_polygon_raster_dyn.png)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false, - "is_executing": true, - "jupyter": { - "outputs_hidden": false - } - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "```Python\n", - "uxds['relhum_200hPa'][0].plot.rasterize(method='point', \n", - " width=1000, \n", - " height=500, \n", - " dynamic=True, xlim=(-68, -60), \n", - " ylim=(-71, -66), \n", - " clim=clim, \n", - " title=\"Regional Point Raster (Dynamic)\")\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "![Point Raster](../images/plots/regional_point_raster_dyn.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "The Polygon Raster can be zoomed in indefinitely, which is due to each polygon covering a region of our screen.\n", - "\n", - "However, zooming in to our Point Rasters exposes how each point is still simply a pair of latitude and longitude coordinates, without any sense of area. After a certain point, there isn't enough points to sample into a uniform looking raster image, and we are left with an approximation the individual points.\n", - "\n", - "\n", - "\n", - "Zooming in even further with the Polygon Raster, we can start to see each individual cell, even at such a high resolution.\n", - "\n", - "![Polygon Raster](../images/plots/polygon_raster_regional_dyn_zoomed.png)" - ] - } - ], - "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.11.5" - }, - "nbdime-conflicts": { - "local_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python 3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ], - "remote_diff": [ - { - "diff": [ - { - "diff": [ - { - "key": 0, - "op": "addrange", - "valuelist": [ - "Python3" - ] - }, - { - "key": 0, - "length": 1, - "op": "removerange" - } - ], - "key": "display_name", - "op": "patch" - } - ], - "key": "kernelspec", - "op": "patch" - } - ] - }, - "toc-autonumbering": false - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/notebooks/03-uxarray-vis/07-animations.ipynb b/notebooks/03-uxarray-vis/07-animations.ipynb deleted file mode 100644 index 3cd3505f..00000000 --- a/notebooks/03-uxarray-vis/07-animations.ipynb +++ /dev/null @@ -1,55 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "766ae3323db40ec2", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "# Interactive & Animated Plots\n", - "---" - ] - }, - { - "cell_type": "markdown", - "id": "135881e235d90c09", - "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false - } - }, - "source": [ - "
\n", - "

Note:

\n", - " This notebook is currently under development! Please refer to the Interactive HoloViz Visualization \n", - "
" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/04-recipes/.mpas-ocean.ipynb.layout b/notebooks/04-recipes/.mpas-ocean.ipynb.layout new file mode 100644 index 00000000..1e1ea32c --- /dev/null +++ b/notebooks/04-recipes/.mpas-ocean.ipynb.layout @@ -0,0 +1 @@ +{"cells": {"e32144b021f5669a": {"width": 100, "height": 573.7625122070312, "visible": true}, "40492f22f026ef76": {"width": 100, "height": 696, "visible": true}, "6d70f1f4-12bc-4c91-a49c-cbaed203ca1a": {"width": 100, "height": 672, "visible": true}}, "order": ["e32144b021f5669a", "40492f22f026ef76", "6d70f1f4-12bc-4c91-a49c-cbaed203ca1a"]} diff --git a/notebooks/04-recipes/e3sm.ipynb b/notebooks/04-recipes/e3sm.ipynb new file mode 100644 index 00000000..c1fb21e4 --- /dev/null +++ b/notebooks/04-recipes/e3sm.ipynb @@ -0,0 +1,256 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "480d31e75354650d", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# E3SM Atmosphere \n", + "\n", + "This recipe demonstrates how to analyze unstructured grid output from the Energy Exascale Earth System Model (E3SM) using UXarray. Unlike traditional approaches that require regridding, UXarray enables direct analysis of the native model output, preserving the full fidelity of the simulation data.\n", + "\n", + "## Objectives\n", + "\n", + "This recipe guides you through calculating and visualizing key atmospheric radiation metrics:\n", + "\n", + "1. Shortwave Cloud Radiative Effect (SWCRE)\n", + "2. Longwave Cloud Radiative Effect (LWCRE)\n", + "3. Net Cloud Radiative Effect (NetCRE)\n", + "\n", + "The workflow includes techniques for:\n", + "\n", + "- Computing radiation components directly on the native unstructured grid\n", + "- Visualizing spatial patterns between two time steps\n", + "- Analyzing temporal evolution through difference plots\n", + "\n", + "\n", + "-----" + ] + }, + { + "cell_type": "code", + "id": "b831f927ed134d7e", + "metadata": {}, + "source": [ + "import cartopy.crs as ccrs\n", + "import holoviews as hv\n", + "import uxarray as ux\n", + "\n", + "hv.extension(\"bokeh\")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Data\n", + "\n", + "The example uses output from an E3SMv2 atmosphere-only (AMIP) simulation with the following specifications:\n", + "\n", + "- Simulation Period: 6 years\n", + "- Configuration: Present-day control forcing (F2010)\n", + "- Resolution: 1-degree horizontal (ne30pg2)\n", + "- Boundary Conditions: Default E3SMv2 settings for sea surface temperatures and sea ice, cycling annually" + ], + "id": "d87d469db62575a3" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "grid_path = \"../../meshfiles/ne30pg2.grid.nc\"\n", + "data_path = \"../../meshfiles/ne30pg2.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)" + ], + "id": "7382203e13e8533b", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Calculating Shortwave Cloud Radiative Effect (SWCRE)\n", + "\n", + "Shortwave cloud radiative effect can be approximated as the difference beteween all-sky net shortwave flux (FSNT) at the top of the model and the clear-sky net shortwave flux (FSNTC).\n", + "\n", + "$$SWCRE = FSNT - FSNTC$$\n", + "\n" + ], + "id": "68f9976f957933ae" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"SWCRE\"] = uxds[\"FSNT\"] - uxds[\"FSNTC\"]\n", + "uxds[\"SWCRE\"]" + ], + "id": "f91d13dd7567cdda", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Calculating Longwave Cloud Radiative Effect (LWCRE)\n", + "\n", + "Longwave cloud radiative effect is similar to that for SWCRE, but the all-sky and clear-sky longwave fluxes are applied instead.\n", + "\n", + "$$LWCRE = FLUT - FLUTC$$" + ], + "id": "15147661043ff38" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"LWCRE\"] = uxds[\"FLUT\"] - uxds[\"FLUTC\"]\n", + "uxds[\"LWCRE\"]" + ], + "id": "b1f7a5c84d773935", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "## Calculating Net Cloud Radiative Effect (NetCRE)\n", + "\n", + "Net cloud radiative effect is thus the difference beteween shortwave and longwave cloud radiative effect.\n", + "\n", + "$$netCRE = SWCRE - LWCRE$$" + ], + "id": "99782d8595d79ce5" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "uxds[\"netCRE\"] = uxds[\"SWCRE\"] - uxds[\"LWCRE\"]\n", + "uxds[\"netCRE\"]" + ], + "id": "da1da3f5be008cd9", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Plotting\n", + "\n", + "Having computed the Net Cloud Radiative Effect (NetCRE), we can now create visualizations to examine the spatial patterns and temporal evolution of cloud-radiation interactions. Our visualization approach focuses on three key aspects:\n", + "\n", + "1. Initial conditions represented by the first timestep\n", + "2. Final conditions from the last timestep \n", + "3. The difference between these states to reveal temporal changes\n", + "\n", + "The following sections demonstrate how to create these visualizations while maintaining the native unstructured grid structure, ensuring we preserve the full resolution of our simulation data. Below we declare our desired projection.\n" + ], + "id": "3d9a1bef36af59e4" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "projection = ccrs.Robinson()" + ], + "id": "cdfacf0d558f0b0", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Visualizing Different Time Steps\n", + "\n", + "The following visualization displays the Net Cloud Radiative Effect at two critical points in our model output: the first January (initial state) and the final month of January. This comparison allows us to examine both the baseline conditions and the evolved state of cloud-radiation interactions.\n" + ], + "id": "7b22d758f9d5c36e" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(\n", + " uxds[\"netCRE\"]\n", + " .isel(time=0)\n", + " .plot(\n", + " projection=projection,\n", + " pixel_ratio=4.0,\n", + " coastline=True,\n", + " title=\"First Time Step (Year 0 Jan)\",\n", + " )\n", + " + uxds[\"netCRE\"]\n", + " .isel(time=61)\n", + " .plot(\n", + " projection=projection,\n", + " pixel_ratio=4.0,\n", + " coastline=True,\n", + " title=\"Final Time Step (Year 5 Jan)\",\n", + " )\n", + ").cols(1)" + ], + "id": "ed56690664c99508", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Difference Analysis\n", + "\n", + "To quantify and visualize how the Net Cloud Radiative Effect evolved over the simulation period, we compute the difference between the final and initial months of January. This differential analysis highlights regions where cloud-radiation interactions have strengthened or weakened during the simulation.\n" + ], + "id": "85742fa517eb8bdd" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(uxds[\"netCRE\"].isel(time=61) - uxds[\"netCRE\"].isel(time=-0)).plot(\n", + " projection=projection, coastline=True, pixel_ratio=4.0, title=\"Change in NetCRE\"\n", + ")" + ], + "id": "d15916950f596215", + "outputs": [], + "execution_count": null + } + ], + "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 +} diff --git a/notebooks/04-recipes/mpas-atmo.ipynb b/notebooks/04-recipes/mpas-atmo.ipynb new file mode 100644 index 00000000..7d44934d --- /dev/null +++ b/notebooks/04-recipes/mpas-atmo.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9b95bd02-7b6e-41dc-b621-a8ce60fcd4c0", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# MPAS Atmosphere \n", + "\n", + "This recipe demonstrates how to create visualizations using 30km MPAS Atmosphere model output. We'll explore techniques for visualizing atmospheric variables on both the primal and dual MPAS grids, focusing on relative humidity and vorticity at the 200hPa pressure level.\n", + "\n", + "## Visualization Objectives\n", + "\n", + "This recipe will guide you through:\n", + "\n", + "* Creating polygon plots using the MPAS primal grid to visualize relative humidity at 200hPa\n", + "* Developing polygon plots using the MPAS dual grid to visualize vorticity at 200hPa\n", + "* Understanding the differences between primal and dual grid visualizations in MPAS\n", + "\n", + "-----" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import uxarray as ux\n", + "import cartopy.crs as ccrs" + ], + "id": "7e23926af8d369cb", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Relative Humidity\n", + "\n", + "For visualizing relative humidity, we use the Primal MPAS grid, which is composed of hexagons." + ], + "id": "2b1e2c04f594d5a6" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "grid_path = \"../../meshfiles/x1.655362.grid.nc\"\n", + "data_path = \"../../meshfiles/x1.655362.data.nc\"\n", + "\n", + "uxds_primal = ux.open_dataset(grid_path, data_path)\n", + "uxds_primal" + ], + "id": "307abb5eaee01f77", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "uxds_primal['relhum_200hPa'][0].plot(projection=ccrs.Robinson(), backend='matplotlib', pixel_ratio=4.0, features=['coastline'], width=1000, height=500, cmap='viridis', title=\"30km Relative Humidity (MPAS Primal Grid)\")", + "id": "14e180939c0d0fa5", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Vorticity\n", + "\n", + "For visualizing relative humidity, we use the Dual MPAS grid, which is composed of triangles." + ], + "id": "51d7e02ab9e096e3" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "grid_path = \"../../meshfiles/x1.655362.grid.nc\"\n", + "data_path = \"../../meshfiles/x1.655362.data.nc\"\n", + "\n", + "uxds_dual = ux.open_dataset(grid_path, data_path, use_dual=True)\n", + "uxds_dual['vorticity_200hPa']" + ], + "id": "3d7819b5474ba97a", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "uxds_dual['vorticity_200hPa'][0].plot(projection=ccrs.Robinson(), rasterize=True, backend='matplotlib', pixel_ratio=4.0, features=['coastline'], width=1000, height=500, cmap='coolwarm', title=\"30km Vorticity (MPAS Dual Grid)\", clim=(-0.0001,0.0001))", + "id": "7f6231de28c269c8", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## MPAS Dual & Primal Grids\n", + "\n", + "The Model for Prediction Across Scales (MPAS) utilizes two complementary grid structures for atmospheric modeling: the primal grid and the dual grid. The primal grid consists of hexagonal cells that form the primary computational mesh, while the dual grid is composed of triangular cells that connect the centers of the primary hexagons.\n", + "\n", + "In the primal grid structure, scalar quantities like relative humidity are naturally represented at the centers of the hexagonal cells. The dual grid, with its triangular elements, is particularly well-suited for vector quantities and derived fields such as vorticity.\n", + "\n", + "Below, we visualize both grid structures to illustrate their complementary nature. " + ], + "id": "25a606085387a2f5" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(uxds_primal.uxgrid.subset.bounding_box(lon_bounds = (-1, 1), lat_bounds=(-0.5, 0.5)).plot(title=\"MPAS Primal Grid Structure\", ) + \n", + " uxds_dual.uxgrid.subset.bounding_box(lon_bounds = (-1, 1), lat_bounds=(-0.5, 0.5)).plot(title=\"MPAS Dual Grid Structure\")).cols(1).opts(fig_size=200)" + ], + "id": "2624f18a782c713e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The visualization below demonstrates the intricate geometric relationship between MPAS primal and dual grids. By overlaying both grid structures, we can observe how the vertices of each hexagonal cell in the primal grid serve as the cell centers for the triangular elements of the dual grid. Conversely, the vertices of the triangular cells in the dual grid correspond to the centers of the hexagonal cells in the primal grid.", + "id": "95378ec9579e1527" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "(uxds_primal.uxgrid.subset.bounding_box(lon_bounds = (-1, 1), lat_bounds=(-0.5, 0.5)).plot() * \n", + " uxds_dual.uxgrid.subset.bounding_box(lon_bounds = (-1, 1), lat_bounds=(-0.5, 0.5)).plot()).opts(fig_size=200, title=\"Primal & Dual Grid Together\")" + ], + "id": "154bc1d90dff5c4e", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## 3.75km Visualization\n", + "\n", + "For another example of MPAS grid visualization, readers can refer to the UXarray [documentation](https://uxarray.readthedocs.io/en/latest/examples/visualization/3_75km_mpas.html) showcasing a 3.75km resolution grid\n", + "\n", + "This example demonstrates MPAS visualization at a higher resolution compared to the 30km grid shown in this recipe." + ], + "id": "6814b54e61ead2df" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "name": "python3", + "language": "python" + }, + "language_info": { + "name": "" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/04-recipes/mpas-ocean.ipynb b/notebooks/04-recipes/mpas-ocean.ipynb new file mode 100644 index 00000000..35cd7387 --- /dev/null +++ b/notebooks/04-recipes/mpas-ocean.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e32144b021f5669a", + "metadata": { + "panel-layout": { + "height": 573.7625122070312, + "visible": true, + "width": 100 + } + }, + "source": [ + "\"UXarray\n", + "\n", + "# MPAS Ocean\n", + "\n", + "In this recipe, you will learn how to create visualizations of global ocean depth data using MPAS-Ocean model output. The recipe includes techniques for both global-scale visualization and regional analysis, with a specific focus on the Gulf of Mexico region.\n", + "\n", + "## Visualization Objectives\n", + "\n", + "This recipe will guide you through creating:\n", + "\n", + "* Global ocean visualizations using the Robinson projection with raster-based plotting \n", + "* Regional depth analysis of the Gulf of Mexico using vector polygons\n", + "\n", + "\n", + "-----" + ] + }, + { + "cell_type": "code", + "id": "357ab34065819766", + "metadata": {}, + "source": [ + "import cartopy.crs as ccrs\n", + "import uxarray as ux" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "id": "c7be08740dfdd82e", + "metadata": {}, + "source": [ + "grid_path = \"../../meshfiles/oQU120.grid.nc\"\n", + "data_path = \"../../meshfiles/oQU120.data.nc\"\n", + "\n", + "uxds = ux.open_dataset(grid_path, data_path)" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Data Preparation\n", + "\n", + "### Global Ocean Data\n", + "The global MPAS-Ocean grid data requires no preprocessing for our visualization purposes, as it already encompasses the complete global domain." + ], + "id": "8aa88b99936f899e" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "bottom_depth = uxds[\"bottomDepth\"]\n", + "bottom_depth.uxgrid.n_face" + ], + "id": "ba611c00ef6a807d", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Regional Focus: Gulf of Mexico\n", + "To create a detailed regional visualization of the Gulf of Mexico, we'll perform a spatial subset of our global data. By applying a bounding circle operation centered at 90°W longitude and 25°N latitude with a 9-degree radius, we can focus our analysis specifically on the Gulf region. This subsetting approach significantly reduces the computational demands of our visualization routines.\n" + ], + "id": "90086bf0ad76610b" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "bottom_depth_gulf = bottom_depth.subset.bounding_circle((-90.0, 25.0), r=9)\n", + "bottom_depth_gulf.uxgrid.n_face" + ], + "id": "6376a47b3269f2bf", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Global Ocean Depth Visualization\n", + "\n", + "For our global visualization, we selected the Robinson projection combined with raster plotting to effectively display worldwide ocean depth data. The 4.0 pixel ratio setting ensures clear resolution while maintaining efficient performance across the entire global domain." + ], + "id": "62d1b6ea6613a169" + }, + { + "cell_type": "code", + "id": "40492f22f026ef76", + "metadata": { + "panel-layout": { + "height": 696, + "visible": true, + "width": 100 + } + }, + "source": [ + "bottom_depth.plot.polygons(\n", + " rasterize=True,\n", + " backend=\"matplotlib\",\n", + " projection=ccrs.Robinson(),\n", + " coastline=\"110m\",\n", + " cmap=\"Blues\",\n", + " width=1000,\n", + " pixel_ratio=4.0,\n", + " title=\"Global Ocean Depth\",\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Regional Visualization: Gulf of Mexico\n", + "\n", + "For our detailed analysis of the Gulf of Mexico region, we transition to a vector-based polygon visualization approach. This shift from our global visualization strategy is driven by the reduced computational demands of our regional subset, which contains only 164 faces. Vector polygons offer several advantages at this scale, including precise boundary representation and enhanced detail visualization.\n" + ], + "id": "89eeb54318549aa2" + }, + { + "cell_type": "code", + "id": "6d70f1f4-12bc-4c91-a49c-cbaed203ca1a", + "metadata": { + "panel-layout": { + "height": 672, + "visible": true, + "width": 100 + } + }, + "source": [ + "bottom_depth_gulf.plot(\n", + " rasterize=False,\n", + " backend=\"matplotlib\",\n", + " projection=ccrs.Robinson(),\n", + " features=[\"states\", \"coastline\"],\n", + " cmap=\"Blues\",\n", + " width=500,\n", + " title=\"Gulf of Mexico Depth\",\n", + ")" + ], + "outputs": [], + "execution_count": null + } + ], + "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" + }, + "panel-cell-order": [ + "e32144b021f5669a", + "40492f22f026ef76", + "6d70f1f4-12bc-4c91-a49c-cbaed203ca1a" + ] + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/04-recipes/mpas-regional.ipynb b/notebooks/04-recipes/mpas-regional.ipynb new file mode 100644 index 00000000..1e4b02fa --- /dev/null +++ b/notebooks/04-recipes/mpas-regional.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "71e7698b-5b26-4324-b113-0be2ae6144ea", + "metadata": {}, + "source": [ + "\"UXarray\n", + "\n", + "# Interactive Regional MPAS Reflectivity & Precipitation Dashboard\n", + "\n", + "### In this recipie, you'll visualize:\n", + "\n", + "* TODO\n", + "\n", + "### Required Cookbook Sections\n", + "\n", + "* TODO\n", + "-----\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37d470c4-eadd-4758-8f0d-6e4924d591f6", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "", + "name": "" + }, + "language_info": { + "name": "" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/05-viz-packages/datashader.ipynb b/notebooks/05-viz-packages/datashader.ipynb new file mode 100644 index 00000000..8b521f31 --- /dev/null +++ b/notebooks/05-viz-packages/datashader.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Datashader\n", + "---" + ], + "id": "9320edea162f109d" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-04T20:08:23.394487Z", + "start_time": "2024-11-04T20:08:23.391186Z" + } + }, + "cell_type": "code", + "source": [ + "# todo" + ], + "id": "781f47a67aa5e115", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "ad31d371e468bdf0" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/05-viz-packages/lonboard.ipynb b/notebooks/05-viz-packages/lonboard.ipynb new file mode 100644 index 00000000..b95faf2f --- /dev/null +++ b/notebooks/05-viz-packages/lonboard.ipynb @@ -0,0 +1,42 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Lonboard\n", + "---" + ], + "id": "342496cb5a083961" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "f2548d72573ff8fd" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/05-viz-packages/matplotlib.ipynb b/notebooks/05-viz-packages/matplotlib.ipynb new file mode 100644 index 00000000..7ecbed43 --- /dev/null +++ b/notebooks/05-viz-packages/matplotlib.ipynb @@ -0,0 +1,57 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Matplotlib\n", + "---" + ], + "id": "603ede925041bb57" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-11-04T20:08:11.699420Z", + "start_time": "2024-11-04T20:08:11.696372Z" + } + }, + "cell_type": "code", + "source": [ + "# todo" + ], + "id": "32f286fdac3a7845", + "outputs": [], + "execution_count": 1 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "ed0beab11ed42e3f" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/images/logos/NSF-NCAR_Lockup-UCAR-Dark_102523.svg b/notebooks/images/logos/NSF-NCAR_Lockup-UCAR-Dark_102523.svg index 4eb4ded5..538f8ec9 100644 --- a/notebooks/images/logos/NSF-NCAR_Lockup-UCAR-Dark_102523.svg +++ b/notebooks/images/logos/NSF-NCAR_Lockup-UCAR-Dark_102523.svg @@ -1 +1 @@ - \ No newline at end of file +