Skip to article frontmatterSkip to article content
Project Pythia Logo

Interactive Visualuzation using hvPlot


Overview

ERA-5 Dataset is available from NCAR RDA in netcdf format. A subset of this dataset is processed into Zarr format and available from NCAR RDA endpoints. To learn how you can create Zarr files from NCAR RDA netcdf files, please see this notebook.

By the end of this notebook, you should be able to:

  • Understand the importance for interactive plots and the challenges associated with them

  • Use hvPlot to generate basic interactive plots with Xarray

Prerequisites

ConceptsImportanceNotes
Intro to XarrayNecessary
  • Time to learn: 30 minutes

Imports

import holoviews as hv
import xarray as xr
from holoviews import opts

hv.extension("bokeh")
Loading...

Data

As we mentioned above a subset of NCAR RDA data is available in Zarr format.

rda_url = "https://data.rda.ucar.edu/"
annual_means = rda_url + "pythia_era5_24/annual_means/"
xrds = xr.open_dataset(annual_means + "temp_2m_annual_1940_2023.zarr", engine="zarr")
xrds = xrds.isel(time=slice(0, 5))
xrds.load()
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/mapping.py:155, in FSMap.__getitem__(self, key, default)
    154 try:
--> 155     result = self.fs.cat(k)
    156 except self.missing_exceptions as exc:

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/asyn.py:118, in sync_wrapper.<locals>.wrapper(*args, **kwargs)
    117 self = obj or args[0]
--> 118 return sync(self.loop, func, *args, **kwargs)

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/asyn.py:103, in sync(loop, func, timeout, *args, **kwargs)
    102 elif isinstance(return_result, BaseException):
--> 103     raise return_result
    104 else:

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/asyn.py:56, in _runner(event, coro, result, timeout)
     55 try:
---> 56     result[0] = await coro
     57 except Exception as ex:

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/asyn.py:464, in AsyncFileSystem._cat(self, path, recursive, on_error, batch_size, **kwargs)
    463     if ex:
--> 464         raise ex
    465 if (
    466     len(paths) > 1
    467     or isinstance(path, list)
    468     or paths[0] != self._strip_protocol(path)
    469 ):

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/asyn.py:244, in _run_coros_in_chunks.<locals>._run_coro(coro, i)
    243 try:
--> 244     return await asyncio.wait_for(coro, timeout=timeout), i
    245 except Exception as e:

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/asyncio/tasks.py:507, in wait_for(fut, timeout)
    506 async with timeouts.timeout(timeout):
--> 507     return await fut

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/implementations/http.py:246, in HTTPFileSystem._cat_file(self, url, start, end, **kwargs)
    245     out = await r.read()
--> 246     self._raise_not_found_for_status(r, url)
    247 return out

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/implementations/http.py:228, in HTTPFileSystem._raise_not_found_for_status(self, response, url)
    227 if response.status == 404:
--> 228     raise FileNotFoundError(url)
    229 response.raise_for_status()

FileNotFoundError: https://data.rda.ucar.edu/pythia_era5_24/annual_means/temp_2m_annual_1940_2023.zarr/.zmetadata

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/zarr/storage.py:1446, in FSStore.__getitem__(self, key)
   1445 try:
-> 1446     return self.map[key]
   1447 except self.exceptions as e:

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/fsspec/mapping.py:159, in FSMap.__getitem__(self, key, default)
    158         return default
--> 159     raise KeyError(key) from exc
    160 return result

KeyError: '.zmetadata'

The above exception was the direct cause of the following exception:

KeyError                                  Traceback (most recent call last)
File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/xarray/backends/zarr.py:1862, in _get_open_params(store, mode, synchronizer, group, consolidated, consolidate_on_close, chunk_store, storage_options, zarr_version, use_zarr_fill_value_as_mask, zarr_format)
   1861 try:
-> 1862     zarr_root_group = zarr.open_consolidated(store, **open_kwargs)
   1863 except (ValueError, KeyError):
   1864     # ValueError in zarr-python 3.x, KeyError in 2.x.

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/zarr/convenience.py:1362, in open_consolidated(store, metadata_key, mode, **kwargs)
   1361 # setup metadata store
-> 1362 meta_store = ConsolidatedStoreClass(store, metadata_key=metadata_key)
   1364 # pass through

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/zarr/storage.py:3045, in ConsolidatedMetadataStore.__init__(self, store, metadata_key)
   3044 # retrieve consolidated metadata
-> 3045 meta = json_loads(self.store[metadata_key])
   3047 # check format of consolidated metadata

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/zarr/storage.py:1448, in FSStore.__getitem__(self, key)
   1447 except self.exceptions as e:
-> 1448     raise KeyError(key) from e

KeyError: '.zmetadata'

During handling of the above exception, another exception occurred:

GroupNotFoundError                        Traceback (most recent call last)
File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/xarray/backends/zarr.py:1866, in _get_open_params(store, mode, synchronizer, group, consolidated, consolidate_on_close, chunk_store, storage_options, zarr_version, use_zarr_fill_value_as_mask, zarr_format)
   1865 try:
-> 1866     zarr_root_group = zarr.open_group(store, **open_kwargs)
   1867     emit_user_level_warning(
   1868         "Failed to open Zarr store with consolidated metadata, "
   1869         "but successfully read with non-consolidated metadata. "
   (...)   1879         RuntimeWarning,
   1880     )

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/zarr/hierarchy.py:1578, in open_group(store, mode, cache_attrs, synchronizer, path, chunk_store, storage_options, zarr_version, meta_array)
   1577             raise ContainsArrayError(path)
-> 1578         raise GroupNotFoundError(path)
   1580 elif mode == "w":

GroupNotFoundError: group not found at path ''

The above exception was the direct cause of the following exception:

FileNotFoundError                         Traceback (most recent call last)
Cell In[2], line 3
      1 rda_url = "https://data.rda.ucar.edu/"
      2 annual_means = rda_url + "pythia_era5_24/annual_means/"
----> 3 xrds = xr.open_dataset(annual_means + "temp_2m_annual_1940_2023.zarr", engine="zarr")
      4 xrds = xrds.isel(time=slice(0, 5))
      5 xrds.load()

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/xarray/backends/api.py:760, in open_dataset(filename_or_obj, engine, chunks, cache, decode_cf, mask_and_scale, decode_times, decode_timedelta, use_cftime, concat_characters, decode_coords, drop_variables, create_default_indexes, inline_array, chunked_array_type, from_array_kwargs, backend_kwargs, **kwargs)
    748 decoders = _resolve_decoders_kwargs(
    749     decode_cf,
    750     open_backend_dataset_parameters=backend.open_dataset_parameters,
   (...)    756     decode_coords=decode_coords,
    757 )
    759 overwrite_encoded_chunks = kwargs.pop("overwrite_encoded_chunks", None)
--> 760 backend_ds = backend.open_dataset(
    761     filename_or_obj,
    762     drop_variables=drop_variables,
    763     **decoders,
    764     **kwargs,
    765 )
    766 ds = _dataset_from_backend_dataset(
    767     backend_ds,
    768     filename_or_obj,
   (...)    779     **kwargs,
    780 )
    781 return ds

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/xarray/backends/zarr.py:1654, in ZarrBackendEntrypoint.open_dataset(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, use_cftime, decode_timedelta, group, mode, synchronizer, consolidated, chunk_store, storage_options, zarr_version, zarr_format, store, engine, use_zarr_fill_value_as_mask, cache_members)
   1652 filename_or_obj = _normalize_path(filename_or_obj)
   1653 if not store:
-> 1654     store = ZarrStore.open_group(
   1655         filename_or_obj,
   1656         group=group,
   1657         mode=mode,
   1658         synchronizer=synchronizer,
   1659         consolidated=consolidated,
   1660         consolidate_on_close=False,
   1661         chunk_store=chunk_store,
   1662         storage_options=storage_options,
   1663         zarr_version=zarr_version,
   1664         use_zarr_fill_value_as_mask=None,
   1665         zarr_format=zarr_format,
   1666         cache_members=cache_members,
   1667     )
   1669 store_entrypoint = StoreBackendEntrypoint()
   1670 with close_on_error(store):

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/xarray/backends/zarr.py:714, in ZarrStore.open_group(cls, store, mode, synchronizer, group, consolidated, consolidate_on_close, chunk_store, storage_options, append_dim, write_region, safe_chunks, align_chunks, zarr_version, zarr_format, use_zarr_fill_value_as_mask, write_empty, cache_members)
    688 @classmethod
    689 def open_group(
    690     cls,
   (...)    707     cache_members: bool = True,
    708 ):
    709     (
    710         zarr_group,
    711         consolidate_on_close,
    712         close_store_on_close,
    713         use_zarr_fill_value_as_mask,
--> 714     ) = _get_open_params(
    715         store=store,
    716         mode=mode,
    717         synchronizer=synchronizer,
    718         group=group,
    719         consolidated=consolidated,
    720         consolidate_on_close=consolidate_on_close,
    721         chunk_store=chunk_store,
    722         storage_options=storage_options,
    723         zarr_version=zarr_version,
    724         use_zarr_fill_value_as_mask=use_zarr_fill_value_as_mask,
    725         zarr_format=zarr_format,
    726     )
    728     return cls(
    729         zarr_group,
    730         mode,
   (...)    739         cache_members=cache_members,
    740     )

File ~/micromamba/envs/ERA5_interactive/lib/python3.13/site-packages/xarray/backends/zarr.py:1882, in _get_open_params(store, mode, synchronizer, group, consolidated, consolidate_on_close, chunk_store, storage_options, zarr_version, use_zarr_fill_value_as_mask, zarr_format)
   1867             emit_user_level_warning(
   1868                 "Failed to open Zarr store with consolidated metadata, "
   1869                 "but successfully read with non-consolidated metadata. "
   (...)   1879                 RuntimeWarning,
   1880             )
   1881         except missing_exc as err:
-> 1882             raise FileNotFoundError(
   1883                 f"No such file or directory: '{store}'"
   1884             ) from err
   1886 # but the user should still receive a DataTree whose root is the group they asked for
   1887 if group and group != "/":

FileNotFoundError: No such file or directory: 'https://data.rda.ucar.edu/pythia_era5_24/annual_means/temp_2m_annual_1940_2023.zarr'

Considerations for Interactive Plots

Add some markdown text on some of the following ideas:

  • What are some reasons we want to make data visualuzation interactive?

Baisc Interactivity using hvPlot

The hvPlot package is a familiar and high level API for data exploration and visualuzation.

One of the most powerfull features of hvPlot is that it provides an alternative plotting API that directly attaches to existing Python objects through the .hvplot() attribute. For the case of Xarray, importing hvplot.xarray adds a brand new set of plotting routines accessible either through xr.DataArray.hvplot() or xr.Dataset.hvplot()

import hvplot.xarray

Before using hvPlot, let’s take a look at the default Xarray plotting methods.

xrds["VAR_2T"].plot()

We can replace the .plot() function call with .hvplot(). By default, hvPlot uses the Bokeh backend, which has naitive interactive tools, such as :

  • Panning

  • Box Select

  • Scroll Zoom

  • Saving

  • Resetting

xrds["VAR_2T"].hvplot()

If we wanted to plot ...

xrds["VAR_2T"].isel(time=0).plot()

Switching

xrds["VAR_2T"].isel(time=0).hvplot()

Time Widget

Climate data typically comes with multiple timesteps. We can create a basic widget that allows us to seek through time by setting the groupby='time' parameter in our .hvplot() call.

xrds["VAR_2T"].hvplot(groupby="time", widget_location="bottom")

You may notice that our colorbar is dynamically changing as we change our time steps. We can fix the colorbar by setting a clim value, which is a tuple of the minimum and maximum desired colorbar range.

One suggestion is to use the minimum and maximum of the data variable you are visualuzing across time.

clim = (xrds["VAR_2T"].values.min(), xrds["VAR_2T"].values.max())
xrds["VAR_2T"].hvplot(clim=clim, groupby="time", widget_location="bottom")

You may have noticed that there is a slight lag when switching time steps. This is due to hvPlot plotting the full resolution of our dataset. We can instead rasterize the output by setting rasterize=True, which will significantly improve the perfromance of our interactive plot.

xrds["VAR_2T"].hvplot(
    rasterize=True, clim=clim, groupby="time", widget_location="bottom"
)

Animation Widget

Another usefull interactive feature is animations. Instead of manually scrolling through time, we can set up a widget that lets us animate our data across time. This can be achieved by adding a Scrubber widget to our plot by setting widget_type="scrubber"

xrds["VAR_2T"].hvplot(
    rasterize=True,
    groupby="time",
    widget_type="scrubber",
    widget_location="bottom",
)