diff --git a/docs/api.rst b/docs/api.rst index a8ec3c3d2..aba6bc61e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -318,6 +318,10 @@ Grid Grid.subset.nearest_neighbor Grid.subset.bounding_box Grid.subset.bounding_circle + Grid.subset.constant_latitude + Grid.subset.constant_longitude + Grid.subset.constant_latitude_interval + Grid.subset.constant_longitude_interval UxDataArray @@ -331,38 +335,27 @@ UxDataArray UxDataArray.subset.nearest_neighbor UxDataArray.subset.bounding_box UxDataArray.subset.bounding_circle + UxDataArray.subset.constant_latitude + UxDataArray.subset.constant_longitude + UxDataArray.subset.constant_latitude_interval + UxDataArray.subset.constant_longitude_interval Cross Sections -------------- +.. seealso:: -Grid -~~~~ - -.. autosummary:: - :toctree: generated/ - :template: autosummary/accessor_method.rst - - Grid.cross_section - Grid.cross_section.constant_latitude - Grid.cross_section.constant_longitude - Grid.cross_section.constant_latitude_interval - Grid.cross_section.constant_longitude_interval + `Cross Sections User Guide Section `_ -UxDataArray -~~~~~~~~~~~ .. autosummary:: :toctree: generated/ :template: autosummary/accessor_method.rst UxDataArray.cross_section - UxDataArray.cross_section.constant_latitude - UxDataArray.cross_section.constant_longitude - UxDataArray.cross_section.constant_latitude_interval - UxDataArray.cross_section.constant_longitude_interval + Remapping --------- diff --git a/docs/user-guide/cross-sections.ipynb b/docs/user-guide/cross-sections.ipynb index 7cf59335d..29a8cf329 100644 --- a/docs/user-guide/cross-sections.ipynb +++ b/docs/user-guide/cross-sections.ipynb @@ -7,273 +7,180 @@ "source": [ "# Cross-Sections\n", "\n", - "This section demonstrates how to extract cross-sections from an unstructured grid using UXarray, which allows the analysis and visualization across slices of grids. Cross-sections can be performed directly on a `ux.Grid` object or on a `ux.UxDataArray`\n" + "UXarray allows for cross‑sections to be performed along arbitrary great‑circle arcs (GCAs) and lines of constant latitude or longitude. This enables workflows such as vertical or temporal cross-section visualizations.\n", + "\n", + "The data variable is sampled along the cross‑section, and the result is an `xarray.DataArray` (no longer a `uxarray.UxDataArray`) since the output is detached from the original unstructured grid.\n" ] }, { "cell_type": "code", - "execution_count": null, - "id": "b35ba4a2c30750e4", + "id": "16db9f880115ac2b", "metadata": {}, - "outputs": [], "source": [ - "import cartopy.crs as ccrs\n", - "import geoviews as gv\n", - "import geoviews.feature as gf\n", - "\n", - "import uxarray as ux\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", "\n", - "projection = ccrs.Robinson()" - ] + "import uxarray as ux" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "id": "b4160275c09fe6b0", + "id": "720c4a345d659fd3", "metadata": {}, - "outputs": [], "source": [ - "base_path = \"../../test/meshfiles/ugrid/outCSne30/\"\n", - "grid_path = base_path + \"outCSne30.ug\"\n", - "data_path = base_path + \"outCSne30_vortex.nc\"\n", + "def set_lon_lat_xticks(ax, cross_section, n_ticks=6):\n", + " \"\"\"Utility function to draw stacked lat/lon points along the sampled cross-section\"\"\"\n", + " da = cross_section\n", "\n", - "uxds = ux.open_dataset(grid_path, data_path)\n", - "uxds[\"psi\"].plot(\n", - " cmap=\"inferno\",\n", - " periodic_elements=\"split\",\n", - " projection=projection,\n", - " title=\"Global Plot\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a7a40958-0a4d-47e4-9e38-31925261a892", - "metadata": {}, - "source": [ - "## Constant Latitude\n", + " N = da.sizes[\"steps\"]\n", + " tick_pos = np.linspace(0, N - 1, n_ticks, dtype=int)\n", + " lons = da[\"lon\"].values[tick_pos]\n", + " lats = da[\"lat\"].values[tick_pos]\n", "\n", - "Cross-sections along constant latitude lines can be obtained by using the ``.cross_section.constant_latitude(lat)`` method. The sliced grid will be made up of the faces that contain at least one edge that intersects with a line of constant latitude.\n" - ] - }, - { - "cell_type": "markdown", - "id": "2fbe9f6e5bb59a17", - "metadata": {}, - "source": [ - "For example, we can obtain a cross-section at 0 degrees latitude by doing the following:" - ] + " tick_labels = []\n", + " for lon, lat in zip(lons, lats):\n", + " lon_dir = \"E\" if lon >= 0 else \"W\"\n", + " lat_dir = \"N\" if lat >= 0 else \"S\"\n", + " tick_labels.append(f\"{abs(lon):.2f}°{lon_dir}\\n{abs(lat):.2f}°{lat_dir}\")\n", + "\n", + " ax.set_xticks(tick_pos)\n", + " ax.set_xticklabels(tick_labels)\n", + "\n", + " ax.set_xlabel(\"Longitude\\nLatitude\")\n", + " plt.tight_layout()\n", + "\n", + " return fig, ax" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "id": "3775daa1-2f1d-4738-bab5-2b69ebd689d9", + "id": "2a42a1aa9249a94f", "metadata": {}, - "outputs": [], "source": [ - "lat = 0\n", + "grid_path = \"../../test/meshfiles/scrip/ne30pg2/grid.nc\"\n", + "data_path = \"../../test/meshfiles/scrip/ne30pg2/data.nc\"\n", "\n", - "uxda_constant_lat = uxds[\"psi\"].cross_section.constant_latitude(lat)" - ] - }, - { - "cell_type": "markdown", - "id": "dcec0b96b92e7f4", - "metadata": {}, - "source": [ - "Since the result is a new ``UxDataArray``, we can directly plot the result to see the cross-section." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "484b77a6-86da-4395-9e63-f5ac56e37deb", - "metadata": {}, + "uxds = ux.open_dataset(grid_path, data_path)\n", + "uxds[\"RELHUM\"]" + ], "outputs": [], - "source": [ - "(\n", - " uxda_constant_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 {lat} degrees latitude\",\n", - " )\n", - " * gf.grid(projection=projection)\n", - ")" - ] + "execution_count": null }, { "cell_type": "markdown", - "id": "c7cca7de4722c121", + "id": "637eaeb7670eea9b", "metadata": {}, "source": [ - "You can also perform operations on the cross-section, such as taking the mean." + "## Arbitrary Great Circle Arc (GCA)\n", + "\n", + "A cross‑section can be performed between two **arbitrary** (lon,lat) points, which will form a geodesic arc." ] }, { "cell_type": "code", - "execution_count": null, - "id": "1cbee722-34a4-4e67-8e22-f393d7d36c99", - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"Global Mean: {uxds['psi'].data.mean()}\")\n", - "print(f\"Mean at {lat} degrees lat: {uxda_constant_lat.data.mean()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "c4a7ee25-0b60-470f-bab7-92ff70563076", + "id": "11d2b717ba274d79", "metadata": {}, "source": [ - "## Constant Longitude\n", + "start_point = (-45, -45)\n", + "end_point = (45, 45)\n", "\n", - "\n", - "Cross-sections along constant longitude lines can be obtained using the ``.cross_section.constant_longitude(lon)`` method. The sliced grid will be made up of the faces that contain at least one edge that intersects with a line of constant longitude.\n" - ] + "cross_section_gca = uxds[\"RELHUM\"].cross_section(\n", + " start=start_point, end=end_point, steps=100\n", + ")" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "id": "f10917ce-568c-4e98-9b9c-d7c3c82e9ba3", + "id": "cf73e86a3ddd57d9", "metadata": {}, - "outputs": [], "source": [ - "lon = 90\n", + "fig, ax = plt.subplots()\n", + "cross_section_gca.plot(ax=ax)\n", "\n", - "uxda_constant_lon = uxds[\"psi\"].cross_section.constant_longitude(lon)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4add3a54-263e-41af-ac97-1e43c9141cb4", - "metadata": {}, + "set_lon_lat_xticks(ax, cross_section_gca)\n", + "ax.set_title(f\"Cross Section between {start_point} and {end_point}\")\n", + "ax.invert_yaxis()" + ], "outputs": [], - "source": [ - "(\n", - " uxda_constant_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 {lon} degrees longitude\",\n", - " periodic_elements=\"split\",\n", - " )\n", - " * gf.grid(projection=projection)\n", - ")" - ] + "execution_count": null }, { "cell_type": "markdown", - "id": "5044b8680d514fdc", + "id": "c2f3ff22dc82d6ce", "metadata": {}, "source": [ - "## Constant Latitude Interval\n", + "## Constant Latitude\n", "\n", - "Cross-sections between two lines of latitudes can be obtained using the ``.cross_section.constant_lats_interval(lats)`` method. The sliced grid will contain faces that are strictly between the latitude interval." + "A constant‐latitude cross‐section samples data along a horizontal line at a fixed latitude.\n" ] }, { "cell_type": "code", - "execution_count": null, - "id": "fc84e47efe2edf96", + "id": "5a7415fa56f86071", "metadata": {}, - "outputs": [], "source": [ - "lats = [-20, 20]\n", - "\n", - "uxda_constant_lat_interval = uxds[\"psi\"].cross_section.constant_latitude_interval(lats)" - ] + "lat = 45\n", + "cross_section_const_lat = uxds[\"RELHUM\"].cross_section(lat=lat, steps=100)" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, - "id": "60232626ba6c74ad", + "id": "b8cfda1b537a059e", "metadata": {}, - "outputs": [], "source": [ - "(\n", - " uxda_constant_lat_interval.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 between {lats[0]} and {lats[1]} degrees latitude\",\n", - " periodic_elements=\"split\",\n", - " )\n", - " * gf.grid(projection=projection)\n", - ")" - ] + "fig, ax = plt.subplots()\n", + "cross_section_const_lat.plot(ax=ax)\n", + "\n", + "set_lon_lat_xticks(ax, cross_section_const_lat)\n", + "ax.set_title(f\"Cross Section at {lat}° latitude.\")\n", + "ax.invert_yaxis()" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", - "id": "4afa3d891a80c597", + "id": "b6ab076677f11637", "metadata": {}, "source": [ - "## Constant Longitude Interval\n", + "## Constant Longitude\n", "\n", - "Cross-sections between two lines of longitude can be obtained using the ``.cross_section.constant_lons_interval(lons)`` method. The sliced grid will contain faces that are strictly between the longitude interval.\n" + "A constant‐longitude cross‐section samples data along a vertical line at a fixed longitude" ] }, { "cell_type": "code", - "execution_count": null, - "id": "b183d15838aaf6bb", + "id": "add5646acb68496e", "metadata": {}, - "outputs": [], "source": [ - "lons = [-25, 25]\n", - "\n", - "uxda_constant_lon_interval = uxds[\"psi\"].cross_section.constant_longitude_interval(lats)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "22793d56701504ce", - "metadata": {}, + "lon = 0\n", + "cross_section_const_lon = uxds[\"RELHUM\"].cross_section(lon=lon, steps=100)" + ], "outputs": [], - "source": [ - "(\n", - " uxda_constant_lon_interval.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 between {lons[0]} and {lons[1]} degrees longitude\",\n", - " periodic_elements=\"split\",\n", - " )\n", - " * gf.grid(projection=projection)\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "54d9eff1-67f1-4691-a3b0-1ee0c874c98f", - "metadata": {}, - "source": [ - "## Arbitrary Great Circle Arc (GCA)" - ] + "execution_count": null }, { - "cell_type": "markdown", - "id": "ea94ff9f-fe86-470d-813b-45f32a633ffc", + "cell_type": "code", + "id": "1999ff933ce37f4e", "metadata": {}, "source": [ - "```{warning}\n", - "Arbitrary great circle arc cross sections are not yet implemented.\n", - "```" - ] + "fig, ax = plt.subplots()\n", + "cross_section_const_lon.plot(ax=ax)\n", + "\n", + "set_lon_lat_xticks(ax, cross_section_const_lon)\n", + "ax.set_title(f\"Cross Section at {lon}° longitude.\")\n", + "ax.invert_yaxis()" + ], + "outputs": [], + "execution_count": null } ], "metadata": { diff --git a/docs/user-guide/subset.ipynb b/docs/user-guide/subset.ipynb index 9a40da036..61845b006 100644 --- a/docs/user-guide/subset.ipynb +++ b/docs/user-guide/subset.ipynb @@ -9,26 +9,29 @@ } }, "source": [ - "# Subsetting\n", + "# Subsetting \n", "\n", - "When working with large grids, it is often desired to obtain a smaller version, typically zoomed into a region of interest. UXarray supports this through grid-informed subsetting operations. This section will discuss the types of ways to subset a grid:\n", + "When working with large unstructured grids, it’s often more efficient to focus on a smaller, region-specific subset rather than the entire global grid. UXarray offers a suite of grid-aware subsetting operations to help you isolate exactly the area you need for your analysis.\n", "1. Nearest Neighbor\n", "2. Bounding Box\n", - "3. Bounding Circle" + "3. Bounding Circle\n", + "4. Constant Latitude/Longtude \n" ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:43.989811Z", + "start_time": "2025-08-06T02:06:43.930068Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:38.638174Z", - "start_time": "2025-05-09T20:27:26.121832Z" } }, + "outputs": [], "source": [ "import warnings\n", "\n", @@ -42,220 +45,7 @@ "plot_opts = {\"width\": 700, \"height\": 350}\n", "hv.extension(\"bokeh\")\n", "warnings.filterwarnings(\"ignore\")" - ], - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Updating file 'registry.txt' from 'https://github.com/NCAR/GeoCAT-datafiles/raw/main/registry.txt' to '/Users/philipc/Library/Caches/geocat'.\n" - ] - }, - { - "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.2/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.2/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.2/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.2/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": "p1011" - } - }, - "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.2/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.2/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" - }, - { - "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.2/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.2/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": 1 + ] }, { "cell_type": "markdown", @@ -273,16 +63,18 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:44.047093Z", + "start_time": "2025-08-06T02:06:44.003536Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:38.709424Z", - "start_time": "2025-05-09T20:27:38.657982Z" } }, + "outputs": [], "source": [ "datafiles = (\n", " geodf.get(\n", @@ -290,65 +82,63 @@ " ),\n", " geodf.get(\"netcdf_files/MPAS/FalkoJudt/dyamond_1/30km/x1.655362.grid_subset.nc\"),\n", ")" - ], - "outputs": [], - "execution_count": 2 + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:44.159051Z", + "start_time": "2025-08-06T02:06:44.056312Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:39.338309Z", - "start_time": "2025-05-09T20:27:38.812217Z" } }, + "outputs": [], "source": [ "uxds = ux.open_dataset(datafiles[1], datafiles[0])" - ], - "outputs": [], - "execution_count": 3 + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:44.169789Z", + "start_time": "2025-08-06T02:06:44.166535Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:39.359906Z", - "start_time": "2025-05-09T20:27:39.347864Z" } }, + "outputs": [], "source": [ "clim = (uxds[\"relhum_200hPa\"][0].values.min(), uxds[\"relhum_200hPa\"][0].values.max())" - ], - "outputs": [], - "execution_count": 4 + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:44.181158Z", + "start_time": "2025-08-06T02:06:44.176300Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:39.375246Z", - "start_time": "2025-05-09T20:27:39.367824Z" } }, + "outputs": [], "source": [ "features = gf.coastline(\n", " projection=ccrs.PlateCarree(), line_width=1, scale=\"50m\"\n", ") * gf.states(projection=ccrs.PlateCarree(), line_width=1, scale=\"50m\")" - ], - "outputs": [], - "execution_count": 5 + ] }, { "cell_type": "markdown", @@ -366,119 +156,23 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:47.844047Z", + "start_time": "2025-08-06T02:06:44.188056Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:52.729554Z", - "start_time": "2025-05-09T20:27:47.585701Z" } }, + "outputs": [], "source": [ "uxds[\"relhum_200hPa\"][0].plot(\n", " rasterize=True, periodic_elements=\"exclude\", title=\"Global Grid\", **plot_opts\n", ") * features" - ], - "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 [x,y] (x_y relhum_200hPa)\n", - " .Coastline.I :Feature [Longitude,Latitude]\n", - " .States.I :Feature [Longitude,Latitude]" - ] - }, - "execution_count": 6, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1013" - } - }, - "output_type": "execute_result" - } - ], - "execution_count": 6 + ] }, { "cell_type": "markdown", @@ -494,32 +188,21 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:48.221699Z", + "start_time": "2025-08-06T02:06:48.218232Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false - }, - "ExecuteTime": { - "end_time": "2025-05-09T20:27:52.838946Z", - "start_time": "2025-05-09T20:27:52.830945Z" } }, + "outputs": [], "source": [ "uxds[\"relhum_200hPa\"][0].values.mean()" - ], - "outputs": [ - { - "data": { - "text/plain": [ - "np.float32(46.819023)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "execution_count": 7 + ] }, { "cell_type": "markdown", @@ -539,31 +222,39 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:48.237240Z", + "start_time": "2025-08-06T02:06:48.234671Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "uxds[\"relhum_200hPa\"].subset" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:48.257315Z", + "start_time": "2025-08-06T02:06:48.254628Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "uxds[\"relhum_200hPa\"].uxgrid.subset" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -581,27 +272,37 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:48.271551Z", + "start_time": "2025-08-06T02:06:48.269239Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "lon_bounds = (-87.6298 - 2, -87.6298 + 2)\n", "lat_bounds = (41.8781 - 2, 41.8781 + 2)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:52.766685Z", + "start_time": "2025-08-06T02:06:48.279500Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "bbox_subset_nodes = uxds[\"relhum_200hPa\"][0].subset.bounding_box(\n", " lon_bounds,\n", @@ -615,9 +316,7 @@ " title=\"Bounding Box Subset (Corner Node Query)\",\n", " **plot_opts,\n", ") * features" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -635,28 +334,34 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "center_coord = [-87.6298, 41.8781]\n", "\n", "r = 2" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:57.720525Z", + "start_time": "2025-08-06T02:06:53.175529Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "bcircle_subset = uxds[\"relhum_200hPa\"][0].subset.bounding_circle(center_coord, r)\n", "\n", @@ -667,9 +372,7 @@ " title=\"Bounding Circle Subset\",\n", " **plot_opts,\n", ") * features" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -687,26 +390,36 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:06:57.821762Z", + "start_time": "2025-08-06T02:06:57.820069Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "center_coord = [-87.6298, 41.8781]" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:07:00.524753Z", + "start_time": "2025-08-06T02:06:58.052234Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "nn_subset = uxds[\"relhum_200hPa\"][0].subset.nearest_neighbor(\n", " center_coord, k=30, element=\"nodes\"\n", @@ -719,9 +432,7 @@ " title=\"Nearest Neighbor Subset (Query 30 closest nodes)\",\n", " **plot_opts,\n", ") * features" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -737,12 +448,18 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:07:02.441636Z", + "start_time": "2025-08-06T02:07:00.535722Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "nn_subset_120 = uxds[\"relhum_200hPa\"][0].subset.nearest_neighbor(\n", " center_coord, k=120, element=\"face centers\"\n", @@ -755,9 +472,7 @@ " title=\"Nearest Neighbor Subset (Query 120 Closest Faces)\",\n", " **plot_opts,\n", ") * features" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -773,12 +488,18 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:07:04.405812Z", + "start_time": "2025-08-06T02:07:02.543197Z" + }, "collapsed": false, "jupyter": { "outputs_hidden": false } }, + "outputs": [], "source": [ "nn_subset_1 = uxds[\"relhum_200hPa\"][0].subset.nearest_neighbor(\n", " center_coord, k=1, element=\"face centers\"\n", @@ -791,58 +512,161 @@ " title=\"Nearest Neighbor Subset (Query Closest Face)\",\n", " **plot_opts,\n", ") * features" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Latitude & Longitude Slices" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Constant Longitude" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false + "ExecuteTime": { + "end_time": "2025-08-06T02:12:03.556161Z", + "start_time": "2025-08-06T02:12:01.688317Z" } }, + "outputs": [], "source": [ - "### Analysis Operators on Regional Subsets\n", + "lon = 0.0\n", "\n", - "Since each subset is a newly initialized ``UxDataArray``, paired also with a newly initialized `Grid`, we can perform analysis operators directly on these new objects.\n", + "clon_subset = uxds[\"relhum_200hPa\"][0].subset.constant_longitude(lon)\n", "\n", - "Looking back at the global mean that we computed earlier, we can compare it to the regional mean of the Bounding Box and Bounding Circle regions respectively." + "clon_subset.plot(\n", + " rasterize=True,\n", + " clim=clim,\n", + " title=\"Constant Longitude Subset\",\n", + " global_extent=True,\n", + " **plot_opts,\n", + ") * features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Constant Latitude" ] }, { "cell_type": "code", + "execution_count": null, "metadata": { - "collapsed": false, - "jupyter": { - "outputs_hidden": false + "ExecuteTime": { + "end_time": "2025-08-06T02:11:44.502932Z", + "start_time": "2025-08-06T02:11:42.655010Z" } }, + "outputs": [], "source": [ - "print(\"Global Mean: \", uxds[\"relhum_200hPa\"][0].values.mean())\n", - "print(\"Bounding Box Mean: \", bbox_subset_nodes.values.mean())\n", - "print(\"Bounding Circle Mean: \", bcircle_subset.values.mean())" - ], + "lat = 0.0\n", + "\n", + "clat_subset = uxds[\"relhum_200hPa\"][0].subset.constant_latitude(lat)\n", + "\n", + "clat_subset.plot(\n", + " rasterize=True,\n", + " clim=clim,\n", + " title=\"Constant Longitude Subset\",\n", + " global_extent=True,\n", + " **plot_opts,\n", + ") * features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Constant Longitude Interval" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:12:41.588158Z", + "start_time": "2025-08-06T02:12:34.241494Z" + } + }, "outputs": [], - "execution_count": null + "source": [ + "lons = (-50, 50)\n", + "\n", + "clon_int_subset = uxds[\"relhum_200hPa\"][0].subset.constant_longitude_interval(lons)\n", + "\n", + "clon_int_subset.plot(\n", + " rasterize=True,\n", + " clim=clim,\n", + " title=\"Constant Latitude Interval Subset\",\n", + " global_extent=True,\n", + " **plot_opts,\n", + ") * features" + ] }, { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Constant Latitude Interval" + ] + }, + { + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], + "source": [ + "lats = (-50, 50)\n", + "\n", + "clat_int_subset = uxds[\"relhum_200hPa\"][0].subset.constant_latitude_interval(lats)\n", + "\n", + "clat_int_subset.plot(\n", + " rasterize=True,\n", + " clim=clim,\n", + " title=\"Constant Latitude Interval Subset\",\n", + " global_extent=True,\n", + " **plot_opts,\n", + ") * features" + ] + }, + { "cell_type": "markdown", - "source": "## Determining if a Grid is a Subset" + "metadata": {}, + "source": [ + "## Determining if a Grid is a Subset" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "To check if a Grid (or dataset using `.uxgrid`) is a subset, we can use `Grid.is_subset`, which will return either `True` or `False`, depending on whether the `Grid` is a subset. Since `nn_subset_120` is a subset, using this feature we will return `True`:" + "source": [ + "To check if a Grid (or dataset using `.uxgrid`) is a subset, we can use `Grid.is_subset`, which will return either `True` or `False`, depending on whether the `Grid` is a subset. Since `nn_subset_120` is a subset, using this feature we will return `True`:" + ] }, { "cell_type": "code", - "metadata": {}, - "source": "nn_subset_120.uxgrid.is_subset", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:07:04.776005Z", + "start_time": "2025-08-06T02:07:04.773458Z" + } + }, "outputs": [], - "execution_count": null + "source": [ + "nn_subset_120.uxgrid.is_subset" + ] }, { "cell_type": "markdown", @@ -853,12 +677,17 @@ }, { "cell_type": "code", - "metadata": {}, + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T02:07:04.805864Z", + "start_time": "2025-08-06T02:07:04.803212Z" + } + }, + "outputs": [], "source": [ "uxds.uxgrid.is_subset" - ], - "outputs": [], - "execution_count": null + ] } ], "metadata": { diff --git a/pyproject.toml b/pyproject.toml index 77660ad92..bb63bc15b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "hvplot", "healpix", "polars", + "pyproj" ] # minimal dependencies end diff --git a/test/meshfiles/scrip/ne30pg2/data.nc b/test/meshfiles/scrip/ne30pg2/data.nc new file mode 100644 index 000000000..cb15eb70d Binary files /dev/null and b/test/meshfiles/scrip/ne30pg2/data.nc differ diff --git a/test/meshfiles/scrip/ne30pg2/grid.nc b/test/meshfiles/scrip/ne30pg2/grid.nc new file mode 100644 index 000000000..ae86055f8 Binary files /dev/null and b/test/meshfiles/scrip/ne30pg2/grid.nc differ diff --git a/test/test_cross_sections.py b/test/test_cross_sections.py index 83958f90d..8988b600c 100644 --- a/test/test_cross_sections.py +++ b/test/test_cross_sections.py @@ -3,6 +3,7 @@ import numpy as np from pathlib import Path import os +import xarray as xr import numpy.testing as nt @@ -13,90 +14,81 @@ quad_hex_node_data = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'random-node-data.nc' cube_sphere_grid = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" -from uxarray.grid.intersections import constant_lat_intersections_face_bounds + +csne8_grid = current_path / "meshfiles" / "scrip" / "ne30pg2" / "grid.nc" +csne8_data = current_path / "meshfiles" / "scrip" / "ne30pg2" / "data.nc" + -def test_repr(): - uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) - # grid repr - grid_repr = uxds.uxgrid.cross_section.__repr__() - assert "constant_latitude" in grid_repr - assert "constant_longitude" in grid_repr - assert "constant_latitude_interval" in grid_repr - assert "constant_longitude_interval" in grid_repr +from uxarray.grid.intersections import constant_lat_intersections_face_bounds + - # data array repr - da_repr = uxds['t2m'].cross_section.__repr__() - assert "constant_latitude" in da_repr - assert "constant_longitude" in da_repr - assert "constant_latitude_interval" in da_repr - assert "constant_longitude_interval" in da_repr -def test_constant_lat_cross_section_grid(): +def test_constant_lat_subset_grid(): uxgrid = ux.open_grid(quad_hex_grid_path) - grid_top_two = uxgrid.cross_section.constant_latitude(lat=0.1) + grid_top_two = uxgrid.subset.constant_latitude(lat=0.1) assert grid_top_two.n_face == 2 - grid_bottom_two = uxgrid.cross_section.constant_latitude(lat=-0.1) + grid_bottom_two = uxgrid.subset.constant_latitude(lat=-0.1) assert grid_bottom_two.n_face == 2 - grid_all_four = uxgrid.cross_section.constant_latitude(lat=0.0) + grid_all_four = uxgrid.subset.constant_latitude(lat=0.0) assert grid_all_four.n_face == 4 with pytest.raises(ValueError): - uxgrid.cross_section.constant_latitude(lat=10.0) + uxgrid.subset.constant_latitude(lat=10.0) -def test_constant_lon_cross_section_grid(): +def test_constant_lon_subset_grid(): uxgrid = ux.open_grid(quad_hex_grid_path) - grid_left_two = uxgrid.cross_section.constant_longitude(lon=-0.1) + grid_left_two = uxgrid.subset.constant_longitude(lon=-0.1) assert grid_left_two.n_face == 2 - grid_right_two = uxgrid.cross_section.constant_longitude(lon=0.2) + grid_right_two = uxgrid.subset.constant_longitude(lon=0.2) assert grid_right_two.n_face == 2 with pytest.raises(ValueError): - uxgrid.cross_section.constant_longitude(lon=10.0) + uxgrid.subset.constant_longitude(lon=10.0) -def test_constant_lat_cross_section_uxds(): +def test_constant_lat_subset_uxds(): uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) uxds.uxgrid.normalize_cartesian_coordinates() - da_top_two = uxds['t2m'].cross_section.constant_latitude(lat=0.1) + da_top_two = uxds['t2m'].subset.constant_latitude(lat=0.1) np.testing.assert_array_equal(da_top_two.data, uxds['t2m'].isel(n_face=[1, 2]).data) - da_bottom_two = uxds['t2m'].cross_section.constant_latitude(lat=-0.1) + da_bottom_two = uxds['t2m'].subset.constant_latitude(lat=-0.1) np.testing.assert_array_equal(da_bottom_two.data, uxds['t2m'].isel(n_face=[0, 3]).data) - da_all_four = uxds['t2m'].cross_section.constant_latitude(lat=0.0) + da_all_four = uxds['t2m'].subset.constant_latitude(lat=0.0) np.testing.assert_array_equal(da_all_four.data, uxds['t2m'].data) with pytest.raises(ValueError): - uxds['t2m'].cross_section.constant_latitude(lat=10.0) + uxds['t2m'].subset.constant_latitude(lat=10.0) -def test_constant_lon_cross_section_uxds(): +def test_constant_lon_subset_uxds(): uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) uxds.uxgrid.normalize_cartesian_coordinates() - da_left_two = uxds['t2m'].cross_section.constant_longitude(lon=-0.1) + da_left_two = uxds['t2m'].subset.constant_longitude(lon=-0.1) np.testing.assert_array_equal(da_left_two.data, uxds['t2m'].isel(n_face=[0, 2]).data) - da_right_two = uxds['t2m'].cross_section.constant_longitude(lon=0.2) + da_right_two = uxds['t2m'].subset.constant_longitude(lon=0.2) np.testing.assert_array_equal(da_right_two.data, uxds['t2m'].isel(n_face=[1, 3]).data) with pytest.raises(ValueError): - uxds['t2m'].cross_section.constant_longitude(lon=10.0) + uxds['t2m'].subset.constant_longitude(lon=10.0) def test_north_pole(): uxgrid = ux.open_grid(cube_sphere_grid) lats = [89.85, 89.9, 89.95, 89.99] for lat in lats: - cross_grid = uxgrid.cross_section.constant_latitude(lat=lat) + cross_grid = uxgrid.subset.constant_latitude(lat=lat) assert cross_grid.n_face == 4 def test_south_pole(): @@ -104,7 +96,7 @@ def test_south_pole(): lats = [-89.85, -89.9, -89.95, -89.99] for lat in lats: - cross_grid = uxgrid.cross_section.constant_latitude(lat=lat) + cross_grid = uxgrid.subset.constant_latitude(lat=lat) assert cross_grid.n_face == 4 def test_constant_lat(): @@ -146,7 +138,7 @@ def test_const_lat_interval_da(): uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) uxds.uxgrid.normalize_cartesian_coordinates() - res = uxds['t2m'].cross_section.constant_latitude_interval(lats=(-10, 10)) + res = uxds['t2m'].subset.constant_latitude_interval(lats=(-10, 10)) assert len(res) == 4 @@ -154,11 +146,11 @@ def test_const_lat_interval_da(): def test_const_lat_interval_grid(): uxgrid = ux.open_grid(quad_hex_grid_path) - res = uxgrid.cross_section.constant_latitude_interval(lats=(-10, 10)) + res = uxgrid.subset.constant_latitude_interval(lats=(-10, 10)) assert res.n_face == 4 - res, indices = uxgrid.cross_section.constant_latitude_interval(lats=(-10, 10), return_face_indices=True) + res, indices = uxgrid.subset.constant_latitude_interval(lats=(-10, 10), return_face_indices=True) assert len(indices) == 4 @@ -166,7 +158,7 @@ def test_const_lon_interva_da(): uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) uxds.uxgrid.normalize_cartesian_coordinates() - res = uxds['t2m'].cross_section.constant_longitude_interval(lons=(-10, 10)) + res = uxds['t2m'].subset.constant_longitude_interval(lons=(-10, 10)) assert len(res) == 4 @@ -174,11 +166,11 @@ def test_const_lon_interva_da(): def test_const_lon_interval_grid(): uxgrid = ux.open_grid(quad_hex_grid_path) - res = uxgrid.cross_section.constant_longitude_interval(lons=(-10, 10)) + res = uxgrid.subset.constant_longitude_interval(lons=(-10, 10)) assert res.n_face == 4 - res, indices = uxgrid.cross_section.constant_longitude_interval(lons=(-10, 10), return_face_indices=True) + res, indices = uxgrid.subset.constant_longitude_interval(lons=(-10, 10), return_face_indices=True) assert len(indices) == 4 @@ -201,13 +193,13 @@ def test_latitude_along_arc(self): -def test_double_cross_section(): +def test_double_subset(): uxgrid = ux.open_grid(quad_hex_grid_path) # construct edges - sub_lat = uxgrid.cross_section.constant_latitude(0.0) + sub_lat = uxgrid.subset.constant_latitude(0.0) - sub_lat_lon = sub_lat.cross_section.constant_longitude(0.0) + sub_lat_lon = sub_lat.subset.constant_longitude(0.0) assert "n_edge" not in sub_lat_lon._ds.dims @@ -215,8 +207,35 @@ def test_double_cross_section(): _ = uxgrid.edge_node_connectivity _ = uxgrid.edge_lon - sub_lat = uxgrid.cross_section.constant_latitude(0.0) + sub_lat = uxgrid.subset.constant_latitude(0.0) - sub_lat_lon = sub_lat.cross_section.constant_longitude(0.0) + sub_lat_lon = sub_lat.subset.constant_longitude(0.0) assert "n_edge" in sub_lat_lon._ds.dims + + +def test_cross_section(): + uxds = ux.open_dataset(csne8_grid, csne8_data) + + # Tributary GCA + ss_gca = uxds['RELHUM'].cross_section(start=(-45, -45), end=(45, 45)) + assert isinstance(ss_gca, xr.DataArray) + + # Constant Latitude + ss_clat = uxds['RELHUM'].cross_section(lat=45) + assert isinstance(ss_clat, xr.DataArray) + + # Constant Longitude + ss_clon = uxds['RELHUM'].cross_section(lon=45) + assert isinstance(ss_clon, xr.DataArray) + + # Constant Longitude with increased samples + ss_clon = uxds['RELHUM'].cross_section(lon=45, steps=3) + assert isinstance(ss_clon, xr.DataArray) + + + with pytest.raises(ValueError): + _ = uxds['RELHUM'].cross_section(end=(45, 45)) + _ = uxds['RELHUM'].cross_section(start=(45, 45)) + _ = uxds['RELHUM'].cross_section(lon=45, end=(45, 45)) + _ = uxds['RELHUM'].cross_section() diff --git a/uxarray/cross_sections/dataarray_accessor.py b/uxarray/cross_sections/dataarray_accessor.py index 93b02a35a..096d95357 100644 --- a/uxarray/cross_sections/dataarray_accessor.py +++ b/uxarray/cross_sections/dataarray_accessor.py @@ -1,211 +1,185 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Set, Tuple, Union +from warnings import warn -if TYPE_CHECKING: - pass +import numpy as np +import xarray as xr +from uxarray.constants import INT_DTYPE + +from .sample import ( + _fill_numba, + sample_constant_latitude, + sample_constant_longitude, + sample_geodesic, +) -class UxDataArrayCrossSectionAccessor: - """Accessor for cross-section operations on a ``UxDataArray``""" +class UxDataArrayCrossSectionAccessor: def __init__(self, uxda) -> None: self.uxda = uxda - def __repr__(self): - prefix = "\n" - methods_heading = "Supported Methods:\n" - - methods_heading += " * constant_latitude(lat, inverse_indices)\n" - methods_heading += " * constant_longitude(lon, inverse_indices)\n" - methods_heading += " * constant_latitude_interval(lats, inverse_indices)\n" - methods_heading += " * constant_longitude_interval(lons, inverse_indices)\n" + def __call__( + self, + *, + start: tuple[float, float] | None = None, + end: tuple[float, float] | None = None, + lat: float | None = None, + lon: float | None = None, + steps: int = 100, + ) -> xr.DataArray: + """ + Extracts a cross-section sampled along an arbitrary great-circle arc (GCA) or line of constant latitude/longitude. - return prefix + methods_heading + Exactly one mode must be specified: - def constant_latitude( - self, lat: float, inverse_indices: Union[List[str], Set[str], bool] = False - ): - """Extracts a cross-section of the data array by selecting all faces that - intersect with a specified line of constant latitude. + - **Great‐circle**: supply both `start` and `end` (lon, lat) tuples, + samples a geodesic arc. + - **Constant latitude**: supply `lat` alone (float), + returns points along that latitude. + - **Constant longitude**: supply `lon` alone (float), + returns points along that longitude. Parameters ---------- - lat : float - The latitude at which to extract the cross-section, in degrees. - Must be between -90.0 and 90.0 - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) + start : tuple[float, float], optional + (lon, lat) of the geodesic start point. + end : tuple[float, float], optional + (lon, lat) of the geodesic end point. + lat : float, optional + Latitude for a constant‐latitude slice. + lon : float, optional + Longitude for a constant‐longitude slice. + steps : int, default 100 + Number of sample points (including endpoints). Returns ------- - uxarray.UxDataArray - A subset of the original data array containing only the faces that intersect - with the specified latitude. + xr.DataArray + A DataArray with a new `"steps"` dimension, plus `"lat"` and `"lon"` + coordinate variables giving the sampling positions. - Raises - ------ - ValueError - If no intersections are found at the specified longitude or the data variable is not face-centered. Examples -------- - >>> # Extract data at 15.5°S latitude - >>> cross_section = uxda.cross_section.constant_latitude(lat=-15.5) - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - if not self.uxda._face_centered(): - raise ValueError( - "Cross sections are only supported for face-centered data variables." - ) - - faces = self.uxda.uxgrid.get_faces_at_constant_latitude(lat) + Cross-section between two points (lon, lat) - return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + >>> uxda.cross_section(start=(-45, -45), end=(45, 45)) - def constant_longitude( - self, lon: float, inverse_indices: Union[List[str], Set[str], bool] = False - ): - """Extracts a cross-section of the data array by selecting all faces that - intersect with a specified line of constant longitude. + Constant latitude cross-section - Parameters - ---------- - lon : float - The latitude at which to extract the cross-section, in degrees. - Must be between -180.0 and 180.0 - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) + >>> uxda.cross_section(lat=45) - Returns - ------- - uxarray.UxDataArray - A subset of the original data array containing only the faces that intersect - with the specified longitude. + Constant longitude cross-section - Raises - ------ - ValueError - If no intersections are found at the specified longitude or the data variable is not face-centered. + >>> uxda.cross_section(lon=0) - Examples - -------- - >>> # Extract data at 0° longitude - >>> cross_section = uxda.cross_section.constant_latitude(lon=0.0) + Constant longitude cross-section with custom number of steps - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. + >>> uxda.cross_section(lon=0, steps=200) """ - if not self.uxda._face_centered(): - raise ValueError( - "Cross sections are only supported for face-centered data variables." - ) - faces = self.uxda.uxgrid.get_faces_at_constant_longitude( - lon, - ) + if steps < 2: + raise ValueError("steps must be at least 2") - return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + great_circle = start is not None or end is not None + const_lon = lon is not None + const_lat = lat is not None - def constant_latitude_interval( - self, - lats: Tuple[float, float], - inverse_indices: Union[List[str], Set[str], bool] = False, - ): - """Extracts a cross-section of data by selecting all faces that - are within a specified latitude interval. - - Parameters - ---------- - lats : Tuple[float, float] - The latitude interval (min_lat, max_lat) at which to extract the cross-section, - in degrees. Values must be between -90.0 and 90.0 - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) - - Returns - ------- - uxarray.UxDataArray - A subset of the original data array containing only the faces that are within a specified latitude interval. - - Raises - ------ - ValueError - If no faces are found within the specified latitude interval. - - Examples - -------- - >>> # Extract data between 30°S and 30°N latitude - >>> cross_section = uxda.cross_section.constant_latitude_interval( - ... lats=(-30.0, 30.0) - ... ) - - - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - faces = self.uxda.uxgrid.get_faces_between_latitudes(lats) + if great_circle and (start is None or end is None): + raise ValueError( + "Both 'start' and 'end' must be provided for great-circle mode." + ) - return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + # exactly one mode + if sum([great_circle, const_lon, const_lat]) != 1: + raise ValueError( + "Must specify exactly one mode (keyword-only): start & end, OR lon, OR lat." + ) - def constant_longitude_interval( - self, - lons: Tuple[float, float], - inverse_indices: Union[List[str], Set[str], bool] = False, - ): - """Extracts a cross-section of data by selecting all faces are within a specifed longitude interval. + if great_circle: + points_xyz, points_latlon = sample_geodesic(start, end, steps) + elif const_lat: + points_xyz, points_latlon = sample_constant_latitude(lat, steps) + else: + points_xyz, points_latlon = sample_constant_longitude(lon, steps) - Parameters - ---------- - lons : Tuple[float, float] - The longitude interval (min_lon, max_lon) at which to extract the cross-section, - in degrees. Values must be between -180.0 and 180.0 - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) + # Find the nearest face for each sample (–1 if no face) + faces = self.uxda.uxgrid.get_faces_containing_point( + points_xyz, return_counts=False + ) + face_idx = np.array([row[0] if row else -1 for row in faces], dtype=INT_DTYPE) + + orig_dims = list(self.uxda.dims) + face_axis = orig_dims.index("n_face") + new_dim = "steps" + new_dims = [new_dim if d == "n_face" else d for d in orig_dims] + dim_axis = new_dims.index(new_dim) + + arr = np.moveaxis(self.uxda.compute().values, face_axis, -1) + M, Nf = arr.reshape(-1, arr.shape[-1]).shape + flat_orig = arr.reshape(M, Nf) + + # Fill along the arc with nearest‐neighbor + flat_filled = _fill_numba(flat_orig, face_idx, Nf, steps) + filled = flat_filled.reshape(*arr.shape[:-1], steps) + + # Move steps axis back to its proper position + data = np.moveaxis(filled, -1, dim_axis) + + # Build coords dict: keep everything except 'n_face' + coords = {d: self.uxda.coords[d] for d in self.uxda.coords if d != "n_face"} + # index along the arc + coords[new_dim] = np.arange(steps) + + # attach lat/lon vectors + coords["lat"] = (new_dim, points_latlon[:, 1]) + coords["lon"] = (new_dim, points_latlon[:, 0]) + + return xr.DataArray( + data, + dims=new_dims, + coords=coords, + name=self.uxda.name, + attrs=self.uxda.attrs, + ) - Returns - ------- - uxarray.UxDataArray - A subset of the original data array containing only the faces that intersect - with the specified longitude interval. + __doc__ = __call__.__doc__ - Raises - ------ - ValueError - If no faces are found within the specified longitude interval. + def constant_latitude(self, *args, **kwargs): + warn( + "The ‘.cross_section.constant_latitude’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_latitude` accessor instead.", + DeprecationWarning, + stacklevel=2, + ) - Examples - -------- - >>> # Extract data between 0° and 45° longitude - >>> cross_section = uxda.cross_section.constant_longitude_interval( - ... lons=(0.0, 45.0) - ... ) - - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - faces = self.uxda.uxgrid.get_faces_between_longitudes(lons) + return self.uxda.subset.constant_latitude(*args, **kwargs) - return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + def constant_longitude(self, *args, **kwargs): + warn( + "The ‘.cross_section.constant_longitude’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_longitude` accessor instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.uxda.subset.constant_longitude(*args, **kwargs) + + def constant_latitude_interval(self, *args, **kwargs): + warn( + "The ‘.cross_section.constant_latitude_interval’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_latitude_interval` accessor instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.uxda.subset.constant_latitude_interval(*args, **kwargs) + + def constant_longitude_interval(self, *args, **kwargs): + warn( + "The ‘.cross_section.constant_longitude_interval’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_longitude_interval` accessor instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.uxda.subset.constant_longitude_interval(*args, **kwargs) diff --git a/uxarray/cross_sections/grid_accessor.py b/uxarray/cross_sections/grid_accessor.py index dfcbe46b7..9cbc48761 100644 --- a/uxarray/cross_sections/grid_accessor.py +++ b/uxarray/cross_sections/grid_accessor.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Set, Tuple, Union +import warnings +from typing import TYPE_CHECKING if TYPE_CHECKING: from uxarray.grid import Grid @@ -12,272 +13,39 @@ class GridCrossSectionAccessor: def __init__(self, uxgrid: Grid) -> None: self.uxgrid = uxgrid - def __repr__(self): - prefix = "\n" - methods_heading = "Supported Methods:\n" - - methods_heading += ( - " * constant_latitude(lat, return_face_indices, inverse_indices)\n" - ) - methods_heading += ( - " * constant_longitude(lon, return_face_indices, inverse_indices)\n" - ) - methods_heading += " * constant_latitude_interval(lats, return_face_indices, inverse_indices)\n" - methods_heading += " * constant_longitude_interval(lons, return_face_indices, inverse_indices)\n" - return prefix + methods_heading - - def constant_latitude( - self, - lat: float, - return_face_indices: bool = False, - inverse_indices: Union[List[str], Set[str], bool] = False, - ): - """Extracts a cross-section of the grid by selecting all faces that - intersect with a specified line of constant latitude. - - Parameters - ---------- - lon : float - The longitude at which to extract the cross-section, in degrees. - Must be between -90.0 and 90.0 - return_face_indices : bool, optional - If True, also returns the indices of the faces that intersect with the - line of constant latitude. - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) - - Returns - ------- - uxarray.Grid - A subset of the original grid containing only the faces that intersect - with the specified latitude. - Tuple[uxarray.Grid, numpy.ndarray], optional - If return_face_indices=True, returns a tuple of (grid_subset, face_indices) - - Raises - ------ - ValueError - If no intersections are found at the specified latitude. - - Examples - -------- - >>> # Extract grid at 25° latitude - >>> cross_section = grid.cross_section.constant_latitude(lat=25.0) - >>> # With face indices - >>> cross_section, faces = grid.cross_section.constant_latitude( - ... lat=25.0, return_face_indices=True - ... ) - - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - - faces = self.uxgrid.get_faces_at_constant_latitude( - lat, - ) - - if len(faces) == 0: - raise ValueError(f"No intersections found at lat={lat}.") - - grid_at_constant_lat = self.uxgrid.isel( - n_face=faces, inverse_indices=inverse_indices - ) - - if return_face_indices: - return grid_at_constant_lat, faces - else: - return grid_at_constant_lat - - def constant_longitude( - self, - lon: float, - return_face_indices: bool = False, - inverse_indices: Union[List[str], Set[str], bool] = False, - ): - """Extracts a cross-section of the grid by selecting all faces that - intersect with a specified line of constant longitude. - - Parameters - ---------- - lon : float - The longitude at which to extract the cross-section, in degrees. - Must be between -180.0 and 180.0 - return_face_indices : bool, optional - If True, also returns the indices of the faces that intersect with the - line of constant longitude. - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) - - Returns - ------- - uxarray.Grid - A subset of the original grid containing only the faces that intersect - with the specified longitude. - Tuple[uxarray.Grid, numpy.ndarray], optional - If return_face_indices=True, returns a tuple of (grid_subset, face_indices) - - Raises - ------ - ValueError - If no intersections are found at the specified longitude. - - Examples - -------- - >>> # Extract grid at 0° longitude (Prime Meridian) - >>> cross_section = grid.cross_section.constant_longitude(lon=0.0) - >>> # With face indices - >>> cross_section, faces = grid.cross_section.constant_longitude( - ... lon=0.0, return_face_indices=True - ... ) - - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - faces = self.uxgrid.get_faces_at_constant_longitude( - lon, + def constant_latitude(self, *args, **kwargs): + warnings.warn( + "The ‘.cross_section.constant_latitude’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_latitude` accessor instead.", + DeprecationWarning, + stacklevel=2, ) - if len(faces) == 0: - raise ValueError(f"No intersections found at lon={lon}") + return self.uxgrid.subset.constant_latitude(*args, **kwargs) - grid_at_constant_lon = self.uxgrid.isel( - n_face=faces, inverse_indices=inverse_indices + def constant_longitude(self, *args, **kwargs): + warnings.warn( + "The ‘.cross_section.constant_longitude’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_longitude` accessor instead.", + DeprecationWarning, + stacklevel=2, ) + return self.uxgrid.subset.constant_longitude(*args, **kwargs) - if return_face_indices: - return grid_at_constant_lon, faces - else: - return grid_at_constant_lon - - def constant_latitude_interval( - self, - lats: Tuple[float, float], - return_face_indices: bool = False, - inverse_indices: Union[List[str], Set[str], bool] = False, - ): - """Extracts a cross-section of the grid by selecting all faces that - are within a specified latitude interval. - - Parameters - ---------- - lats : Tuple[float, float] - The latitude interval (min_lat, max_lat) at which to extract the cross-section, - in degrees. Values must be between -90.0 and 90.0 - return_face_indices : bool, optional - If True, also returns the indices of the faces that intersect with the - latitude interval. - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) - - Returns - ------- - uxarray.Grid - A subset of the original grid containing only the faces that are within a specified latitude interval. - Tuple[uxarray.Grid, numpy.ndarray], optional - If return_face_indices=True, returns a tuple of (grid_subset, face_indices) - - Raises - ------ - ValueError - If no faces are found within the specified latitude interval. - - Examples - -------- - >>> # Extract grid between 30°S and 30°N latitude - >>> cross_section = grid.cross_section.constant_latitude_interval( - ... lats=(-30.0, 30.0) - ... ) - >>> # With face indices - >>> cross_section, faces = grid.cross_section.constant_latitude_interval( - ... lats=(-30.0, 30.0), return_face_indices=True - ... ) - - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - faces = self.uxgrid.get_faces_between_latitudes(lats) - - grid_between_lats = self.uxgrid.isel( - n_face=faces, inverse_indices=inverse_indices + def constant_latitude_interval(self, *args, **kwargs): + warnings.warn( + "The ‘.cross_section.constant_latitude_interval’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_latitude_interval` accessor instead.", + DeprecationWarning, + stacklevel=2, ) + return self.uxgrid.subset.constant_latitude_interval(*args, **kwargs) - if return_face_indices: - return grid_between_lats, faces - else: - return grid_between_lats - - def constant_longitude_interval( - self, - lons: Tuple[float, float], - return_face_indices: bool = False, - inverse_indices: Union[List[str], Set[str], bool] = False, - ): - """Extracts a cross-section of the grid by selecting all faces are within a specifed longitude interval. - - Parameters - ---------- - lons : Tuple[float, float] - The longitude interval (min_lon, max_lon) at which to extract the cross-section, - in degrees. Values must be between -180.0 and 180.0 - return_face_indices : bool, optional - If True, also returns the indices of the faces that intersect are within a specifed longitude interval. - inverse_indices : Union[List[str], Set[str], bool], optional - Controls storage of original grid indices. Options: - - True: Stores original face indices - - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") - - False: No index storage (default) - - Returns - ------- - uxarray.Grid - A subset of the original grid containing only the faces that intersect - with the specified longitude interval. - Tuple[uxarray.Grid, numpy.ndarray], optional - If return_face_indices=True, returns a tuple of (grid_subset, face_indices) - - Raises - ------ - ValueError - If no faces are found within the specified longitude interval. - - Examples - -------- - >>> # Extract grid between 0° and 45° longitude - >>> cross_section = grid.cross_section.constant_longitude_interval( - ... lons=(0.0, 45.0) - ... ) - >>> # With face indices - >>> cross_section, faces = grid.cross_section.constant_longitude_interval( - ... lons=(0.0, 45.0), return_face_indices=True - ... ) - - Notes - ----- - The initial execution time may be significantly longer than subsequent runs - due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. - """ - faces = self.uxgrid.get_faces_between_longitudes(lons) - - grid_between_lons = self.uxgrid.isel( - n_face=faces, inverse_indices=inverse_indices + def constant_longitude_interval(self, *args, **kwargs): + warnings.warn( + "The ‘.cross_section.constant_longitude_interval’ method is deprecated and will be removed in a future release; " + "please use the `.subset.constant_longitude_interval` accessor instead.", + DeprecationWarning, + stacklevel=2, ) - - if return_face_indices: - return grid_between_lons, faces - else: - return grid_between_lons + return self.uxgrid.subset.constant_longitude_interval(*args, **kwargs) diff --git a/uxarray/cross_sections/sample.py b/uxarray/cross_sections/sample.py new file mode 100644 index 000000000..cbf45a6a8 --- /dev/null +++ b/uxarray/cross_sections/sample.py @@ -0,0 +1,116 @@ +import numpy as np +from numba import njit, prange +from pyproj import Geod + + +@njit(parallel=True) +def _fill_numba(flat_orig, face_idx, n_face, n_steps): + M = flat_orig.shape[0] + out = np.full((M, n_steps), np.nan, flat_orig.dtype) + for i in prange(n_steps): + f = face_idx[i] + if 0 <= f < n_face: + out[:, i] = flat_orig[:, f] + return out + + +def sample_geodesic( + start: tuple[float, float], end: tuple[float, float], steps: int +) -> tuple[np.ndarray, np.ndarray]: + lon0, lat0 = start + lon1, lat1 = end + + # validate + for name, val, lo, hi in [ + ("start lon", lon0, -180, 180), + ("start lat", lat0, -90, 90), + ("end lon", lon1, -180, 180), + ("end lat", lat1, -90, 90), + ]: + if not (lo <= val <= hi): + raise ValueError(f"{name}={val} out of bounds [{lo}, {hi}]") + + geod = Geod(ellps="WGS84") + # compute intermediate (lon, lat) points on the ellipsoid + middle = geod.npts(lon0, lat0, lon1, lat1, steps - 2) + + # preallocate arrays + lons = np.empty(steps, dtype=float) + lats = np.empty(steps, dtype=float) + + # endpoints + lons[0], lats[0] = lon0, lat0 + lons[-1], lats[-1] = lon1, lat1 + + # fill middle points + for i, (lon, lat) in enumerate(middle, start=1): + lons[i] = ((lon + 180) % 360) - 180 # normalize to (–180, +180) + lats[i] = lat # geod.npts yields lat in (–90, +90) + + # convert to radians + rad_lat = np.deg2rad(lats) + rad_lon = np.deg2rad(lons) + + # Cartesian coords + x = np.cos(rad_lat) * np.cos(rad_lon) + y = np.cos(rad_lat) * np.sin(rad_lon) + z = np.sin(rad_lat) + points_xyz = np.column_stack([x, y, z]) + + # preserve input order (lon, lat) + points_lonlat = np.column_stack([lons, lats]) + + return points_xyz, points_lonlat + + +def sample_constant_latitude(lat: float, steps: int) -> tuple[np.ndarray, np.ndarray]: + if not (-90.0 <= lat <= 90.0): + raise ValueError(f"Latitude {lat} out of bounds [-90, 90]") + if steps < 2: + raise ValueError(f"steps must be ≥ 2, got {steps}") + + # sample longitudes evenly from –180 to +180 + lons = np.linspace(-180.0, 180.0, steps) + lats = np.full(steps, lat, dtype=float) + + # convert to radians + rad_lon = np.deg2rad(lons) + rad_lat = np.deg2rad(lats) + + # spherical to Cartesian + x = np.cos(rad_lat) * np.cos(rad_lon) + y = np.cos(rad_lat) * np.sin(rad_lon) + z = np.sin(rad_lat) + points_xyz = np.column_stack([x, y, z]) + + # normalize longitudes back into (–180, +180] + norm_lons = ((lons + 180) % 360) - 180 + points_lonlat = np.column_stack([norm_lons, lats]) + + return points_xyz, points_lonlat + + +def sample_constant_longitude(lon: float, steps: int) -> tuple[np.ndarray, np.ndarray]: + if not (-180.0 <= lon <= 180.0): + raise ValueError(f"Longitude {lon} out of bounds [-180, 180]") + if steps < 2: + raise ValueError(f"steps must be ≥ 2, got {steps}") + + # sample latitudes evenly from –90 to +90 + lats = np.linspace(-90.0, 90.0, steps) + lons = np.full(steps, lon, dtype=float) + + # convert to radians + rad_lon = np.deg2rad(lons) + rad_lat = np.deg2rad(lats) + + # spherical to Cartesian + x = np.cos(rad_lat) * np.cos(rad_lon) + y = np.cos(rad_lat) * np.sin(rad_lon) + z = np.sin(rad_lat) + points_xyz = np.column_stack([x, y, z]) + + # lon stays constant, so no need to renormalize + points_lonlat = np.column_stack([lons, lats]) + + return points_xyz, points_lonlat diff --git a/uxarray/subset/dataarray_accessor.py b/uxarray/subset/dataarray_accessor.py index 92b722be9..392e6fae6 100644 --- a/uxarray/subset/dataarray_accessor.py +++ b/uxarray/subset/dataarray_accessor.py @@ -1,12 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, Set, Tuple, Union +from typing import List, Optional, Set, Tuple, Union import numpy as np -if TYPE_CHECKING: - pass - class DataArraySubsetAccessor: """Accessor for performing unstructured grid subsetting with a data @@ -19,13 +16,13 @@ def __repr__(self): prefix = "\n" methods_heading = "Supported Methods:\n" - methods_heading += " * nearest_neighbor(center_coord, k, element, inverse_indices, **kwargs)\n" - methods_heading += ( - " * bounding_circle(center_coord, r, element, inverse_indices, **kwargs)\n" - ) - methods_heading += ( - " * bounding_box(lon_bounds, lat_bounds, inverse_indices, **kwargs)\n" - ) + methods_heading += " * nearest_neighbor(center_coord, k, element)\n" + methods_heading += " * bounding_circle(center_coord, r, element)\n" + methods_heading += " * bounding_box(lon_bounds, lat_bounds)\n" + methods_heading += " * constant_latitude(lat, lon_range)\n" + methods_heading += " * constant_longitude(lon, lat_range)\n" + methods_heading += " * constant_latitude_interval(lats)\n" + methods_heading += " * constant_longitude_interval(lons)\n" return prefix + methods_heading @@ -126,3 +123,210 @@ def nearest_neighbor( ) return self.uxda._slice_from_grid(grid) + + def constant_latitude( + self, + lat: float, + inverse_indices: Union[List[str], Set[str], bool] = False, + lon_range: Tuple[float, float] = (-180, 180), + ): + """Extracts a subset of the data array across a line of constant-latitude. + + Parameters + ---------- + lat : float + The latitude at which to extract the subset, in degrees. + Must be between -90.0 and 90.0 + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + lon_range: Tuple[float, float], optional + `(min_lon, max_lon)` longitude values to perform the subset. Values must lie in [-180, 180]. Default is `(-180, 180)`. + + Returns + ------- + uxarray.UxDataArray + In **grid-based** mode, a subset of the original data array containing only the faces that intersect + with the specified line of constant latitude. + xarray.DataArray + In **interpolated** mode (`interpolate=True`), a new Xarray DataArray with data sampled along the line of constant latitude, + including longitude and latitude coordinates for each sample. + + Raises + ------ + ValueError + If no intersections are found at the specified longitude or the data variable is not face-centered. + + Examples + -------- + >>> # Extract data at 15.5°S latitude + >>> cross_section = uxda.cross_section.constant_latitude(lat=-15.5) + + """ + if not self.uxda._face_centered(): + raise ValueError( + "Cross sections are only supported for face-centered data variables." + ) + + # TODO: Extend to support constrained ranges + faces = self.uxda.uxgrid.get_faces_at_constant_latitude(lat) + + if len(faces) == 0: + raise ValueError( + f"No faces found that intersect a line of constant latitude at {lat} degrees between {lon_range[0]} and {lon_range[1]} degrees longitude." + ) + + da = self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + + da = da.assign_attrs({"cross_section": True, "constant_latitude": lat}) + + return da + + def constant_longitude( + self, + lon: float, + inverse_indices: Union[List[str], Set[str], bool] = False, + lat_range: Tuple[float, float] = (-90, 90), + ): + """Extracts a subset of the data array across a line of constant-longitude. + + This method supports two modes: + - **grid‐based** (`interpolate=False`, the default): returns exactly those faces + which intersect the line of constant longitude, with a new Grid containing those faces. + - **interpolated** (`interpolate=True`): generates `n_samples` equally‐spaced points + between `lon_range[0]` and `lon_range[1]` and picks whichever face contains each sample point. + + Parameters + ---------- + lon : float + The longitude at which to extract the subset, in degrees. + Must be between -180.0 and 180.0 + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + lat_range: Tuple[float, float], optional + `(min_lat, max_lat)` latitude values to perform the subset. Values must lie in [-90, 90]. Default is `(-90, 90)`. + + Returns + ------- + uxarray.UxDataArray + In **grid-based** mode, a subset of the original data array containing only the faces that intersect + with the specified line of constant longitude. + xarray.DataArray + In **interpolated** mode (`interpolate=True`), a new Xarray DataArray with data sampled along the line of constant longitude, + including longitude and latitude coordinates for each sample. + + Raises + ------ + ValueError + If no intersections are found at the specified longitude or the data variable is not face-centered. + + Examples + -------- + >>> # Extract data at 0° longitude + >>> cross_section = uxda.cross_section.constant_longitude(lon=0.0) + """ + if not self.uxda._face_centered(): + raise ValueError( + "Cross sections are only supported for face-centered data variables." + ) + + # TODO: Extend to support constrained ranges + faces = self.uxda.uxgrid.get_faces_at_constant_longitude( + lon, + ) + + if len(faces) == 0: + raise ValueError( + f"No faces found that intersect a line of constant longitude at {lon} degrees between {lat_range[0]} and {lat_range[1]} degrees latitude." + ) + + da = self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + + da = da.assign_attrs({"cross_section": True, "constant_longitude": lon}) + + return da + + def constant_latitude_interval( + self, + lats: Tuple[float, float], + inverse_indices: Union[List[str], Set[str], bool] = False, + ): + """Extracts a subset of data by selecting all faces that + are within a specified latitude interval. + + Parameters + ---------- + lats : Tuple[float, float] + The latitude interval (min_lat, max_lat) at which to extract the subset, + in degrees. Values must be between -90.0 and 90.0 + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + + Returns + ------- + uxarray.UxDataArray + A subset of the original data array containing only the faces that are within a specified latitude interval. + + Raises + ------ + ValueError + If no faces are found within the specified latitude interval. + + Examples + -------- + >>> # Extract data between 30°S and 30°N latitude + >>> cross_section = uxda.cross_section.constant_latitude_interval( + ... lats=(-30.0, 30.0) + ... ) + """ + faces = self.uxda.uxgrid.get_faces_between_latitudes(lats) + + return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) + + def constant_longitude_interval( + self, + lons: Tuple[float, float], + inverse_indices: Union[List[str], Set[str], bool] = False, + ): + """Extracts a subset of data by selecting all faces are within a specified longitude interval. + + Parameters + ---------- + lons : Tuple[float, float] + The longitude interval (min_lon, max_lon) at which to extract the subset, + in degrees. Values must be between -180.0 and 180.0 + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + + Returns + ------- + uxarray.UxDataArray + A subset of the original data array containing only the faces that intersect + with the specified longitude interval. + + Raises + ------ + ValueError + If no faces are found within the specified longitude interval. + + Examples + -------- + >>> # Extract data between 0° and 45° longitude + >>> cross_section = uxda.cross_section.constant_longitude_interval( + ... lons=(0.0, 45.0) + ... ) + """ + faces = self.uxda.uxgrid.get_faces_between_longitudes(lons) + + return self.uxda.isel(n_face=faces, inverse_indices=inverse_indices) diff --git a/uxarray/subset/grid_accessor.py b/uxarray/subset/grid_accessor.py index 98f4b59ac..4d9cf5d64 100644 --- a/uxarray/subset/grid_accessor.py +++ b/uxarray/subset/grid_accessor.py @@ -19,11 +19,13 @@ def __repr__(self): prefix = "\n" methods_heading = "Supported Methods:\n" - methods_heading += " * nearest_neighbor(center_coord, k, element, inverse_indices, **kwargs)\n" - methods_heading += ( - " * bounding_circle(center_coord, r, element, inverse_indices, **kwargs)\n" - ) - methods_heading += " * bounding_box(lon_bounds, lat_bounds, inverse_indices)\n" + methods_heading += " * nearest_neighbor(center_coord, k, element)\n" + methods_heading += " * bounding_circle(center_coord, r, element)\n" + methods_heading += " * bounding_box(lon_bounds, lat_bounds)\n" + methods_heading += " * constant_latitude(lat, lon_range)\n" + methods_heading += " * constant_longitude(lon, lat_range)\n" + methods_heading += " * constant_latitude_interval(lats)\n" + methods_heading += " * constant_longitude_interval(lons)\n" return prefix + methods_heading @@ -46,11 +48,6 @@ def bounding_box( the antimeridian, otherwise lon_left > lon_right, both between [-180, 180] lat_bounds: tuple, list, np.ndarray (lat_bottom, lat_top) where lat_top > lat_bottom and between [-90, 90] - method: str - Bounding Box Method, currently supports 'coords', which ensures the coordinates of the corner nodes, - face centers, or edge centers lie within the bounds. - element: str - Element for use with `coords` comparison, one of `nodes`, `face centers`, or `edge centers` inverse_indices : Union[List[str], Set[str], bool], optional Controls storage of original grid indices. Options: - True: Stores original face indices @@ -138,6 +135,262 @@ def nearest_neighbor( return self._index_grid(ind, element, inverse_indices=inverse_indices) + def constant_latitude( + self, + lat: float, + return_face_indices: bool = False, + inverse_indices: Union[List[str], Set[str], bool] = False, + ): + """Extracts a subset of the grid by selecting all faces that + intersect with a specified line of constant latitude. + + Parameters + ---------- + lat : float + The latitude at which to extract the subset, in degrees. + Must be between -90.0 and 90.0 + return_face_indices : bool, optional + If True, also returns the indices of the faces that intersect with the + line of constant latitude. + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + + Returns + ------- + uxarray.Grid + A subset of the original grid containing only the faces that intersect + with the specified latitude. + Tuple[uxarray.Grid, numpy.ndarray], optional + If return_face_indices=True, returns a tuple of (grid_subset, face_indices) + + Raises + ------ + ValueError + If no intersections are found at the specified latitude. + + Examples + -------- + >>> # Extract grid at 25° latitude + >>> cross_section = grid.cross_section.constant_latitude(lat=25.0) + >>> # With face indices + >>> cross_section, faces = grid.cross_section.constant_latitude( + ... lat=25.0, return_face_indices=True + ... ) + + Notes + ----- + The initial execution time may be significantly longer than subsequent runs + due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. + """ + + faces = self.uxgrid.get_faces_at_constant_latitude( + lat, + ) + + if len(faces) == 0: + raise ValueError(f"No intersections found at lat={lat}.") + + grid_at_constant_lat = self.uxgrid.isel( + n_face=faces, inverse_indices=inverse_indices + ) + + if return_face_indices: + return grid_at_constant_lat, faces + else: + return grid_at_constant_lat + + def constant_longitude( + self, + lon: float, + return_face_indices: bool = False, + inverse_indices: Union[List[str], Set[str], bool] = False, + ): + """Extracts a subset of the grid by selecting all faces that + intersect with a specified line of constant longitude. + + Parameters + ---------- + lon : float + The longitude at which to extract the subset, in degrees. + Must be between -180.0 and 180.0 + return_face_indices : bool, optional + If True, also returns the indices of the faces that intersect with the + line of constant longitude. + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + + Returns + ------- + uxarray.Grid + A subset of the original grid containing only the faces that intersect + with the specified longitude. + Tuple[uxarray.Grid, numpy.ndarray], optional + If return_face_indices=True, returns a tuple of (grid_subset, face_indices) + + Raises + ------ + ValueError + If no intersections are found at the specified longitude. + + Examples + -------- + >>> # Extract grid at 0° longitude (Prime Meridian) + >>> cross_section = grid.cross_section.constant_longitude(lon=0.0) + >>> # With face indices + >>> cross_section, faces = grid.cross_section.constant_longitude( + ... lon=0.0, return_face_indices=True + ... ) + + Notes + ----- + The initial execution time may be significantly longer than subsequent runs + due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. + """ + faces = self.uxgrid.get_faces_at_constant_longitude( + lon, + ) + + if len(faces) == 0: + raise ValueError(f"No intersections found at lon={lon}") + + grid_at_constant_lon = self.uxgrid.isel( + n_face=faces, inverse_indices=inverse_indices + ) + + if return_face_indices: + return grid_at_constant_lon, faces + else: + return grid_at_constant_lon + + def constant_latitude_interval( + self, + lats: Tuple[float, float], + return_face_indices: bool = False, + inverse_indices: Union[List[str], Set[str], bool] = False, + ): + """Extracts a subset of the grid by selecting all faces that + are within a specified latitude interval. + + Parameters + ---------- + lats : Tuple[float, float] + The latitude interval (min_lat, max_lat) at which to extract the subset, + in degrees. Values must be between -90.0 and 90.0 + return_face_indices : bool, optional + If True, also returns the indices of the faces that intersect with the + latitude interval. + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + + Returns + ------- + uxarray.Grid + A subset of the original grid containing only the faces that are within a specified latitude interval. + Tuple[uxarray.Grid, numpy.ndarray], optional + If return_face_indices=True, returns a tuple of (grid_subset, face_indices) + + Raises + ------ + ValueError + If no faces are found within the specified latitude interval. + + Examples + -------- + >>> # Extract grid between 30°S and 30°N latitude + >>> cross_section = grid.cross_section.constant_latitude_interval( + ... lats=(-30.0, 30.0) + ... ) + >>> # With face indices + >>> cross_section, faces = grid.cross_section.constant_latitude_interval( + ... lats=(-30.0, 30.0), return_face_indices=True + ... ) + + Notes + ----- + The initial execution time may be significantly longer than subsequent runs + due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. + """ + faces = self.uxgrid.get_faces_between_latitudes(lats) + + grid_between_lats = self.uxgrid.isel( + n_face=faces, inverse_indices=inverse_indices + ) + + if return_face_indices: + return grid_between_lats, faces + else: + return grid_between_lats + + def constant_longitude_interval( + self, + lons: Tuple[float, float], + return_face_indices: bool = False, + inverse_indices: Union[List[str], Set[str], bool] = False, + ): + """Extracts a subset of the grid by selecting all faces that are within a specified longitude interval. + + Parameters + ---------- + lons : Tuple[float, float] + The longitude interval (min_lon, max_lon) at which to extract the subset, + in degrees. Values must be between -180.0 and 180.0 + return_face_indices : bool, optional + If True, also returns the indices of the faces that are within a specified longitude interval. + inverse_indices : Union[List[str], Set[str], bool], optional + Controls storage of original grid indices. Options: + - True: Stores original face indices + - List/Set of strings: Stores specified index types (valid values: "face", "edge", "node") + - False: No index storage (default) + + Returns + ------- + uxarray.Grid + A subset of the original grid containing only the faces that intersect + with the specified longitude interval. + Tuple[uxarray.Grid, numpy.ndarray], optional + If return_face_indices=True, returns a tuple of (grid_subset, face_indices) + + Raises + ------ + ValueError + If no faces are found within the specified longitude interval. + + Examples + -------- + >>> # Extract grid between 0° and 45° longitude + >>> cross_section = grid.cross_section.constant_longitude_interval( + ... lons=(0.0, 45.0) + ... ) + >>> # With face indices + >>> cross_section, faces = grid.cross_section.constant_longitude_interval( + ... lons=(0.0, 45.0), return_face_indices=True + ... ) + + Notes + ----- + The initial execution time may be significantly longer than subsequent runs + due to Numba's just-in-time compilation. Subsequent calls will be faster due to caching. + """ + faces = self.uxgrid.get_faces_between_longitudes(lons) + + grid_between_lons = self.uxgrid.isel( + n_face=faces, inverse_indices=inverse_indices + ) + + if return_face_indices: + return grid_between_lons, faces + else: + return grid_between_lons + def _get_tree(self, coords, tree_type): """Internal helper for obtaining the desired KDTree or BallTree.""" if coords.ndim > 1: