From 5be13a3c0863e3faed97260bc9e57a24881254c8 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Tue, 9 Apr 2024 08:09:02 -0700 Subject: [PATCH 1/4] feat: create panel with map and control widgets --- IS2view/api.py | 300 ++++++++++++++++++----- IS2view/tools.py | 14 +- README.rst | 1 + doc/source/getting_started/Citations.rst | 1 + environment.yml | 1 + notebooks/IS2-ATL14-Viewer.ipynb | 61 ++--- notebooks/IS2-ATL15-Viewer.ipynb | 52 ++-- requirements.txt | 1 + 8 files changed, 308 insertions(+), 123 deletions(-) diff --git a/IS2view/api.py b/IS2view/api.py index 1faf946..ee3e977 100644 --- a/IS2view/api.py +++ b/IS2view/api.py @@ -1,7 +1,7 @@ #!/usr/bin/env python u""" api.py -Written by Tyler Sutterley (03/2024) +Written by Tyler Sutterley (04/2024) Plotting tools for visualizing rioxarray variables on leaflet maps PYTHON DEPENDENCIES: @@ -28,6 +28,8 @@ https://xyzservices.readthedocs.io/en/stable/ UPDATE HISTORY: + Updated 04/2024: add connections and functions for changing variables + and other attributes of the leaflet map visualization Updated 03/2024: add fix for broken xyzservice links fix deprecation of copying ipyleaflet layers Updated 11/2023: setting dynamic colormap with float64 min and max @@ -73,6 +75,9 @@ import matplotlib.colorbar import matplotlib.pyplot as plt import matplotlib.colors as colors + matplotlib.rcParams['font.family'] = 'sans-serif' + matplotlib.rcParams['font.sans-serif'] = ['Arial', 'Helvetica', 'DejaVu Sans'] + matplotlib.rcParams['mathtext.default'] = 'regular' except (AttributeError, ImportError, ModuleNotFoundError) as exc: logging.critical("matplotlib not available") try: @@ -316,7 +321,7 @@ def __init__(self, projection, **kwargs): if kwargs['cursor_control']: self.cursor = ipywidgets.Label() cursor_control = ipyleaflet.WidgetControl(widget=self.cursor, - position='bottomright') + position='bottomleft') self.map.add(cursor_control) # keep track of cursor position self.map.on_interaction(self.handle_interaction) @@ -671,11 +676,11 @@ def boundary_change(self, change): """Update image on boundary change """ # add image object to map - if self.image is not None: + if self._image is not None: # attempt to remove layer - self.remove(self.image) + self.remove(self._image) # create new image service layer - self.image = ipyleaflet.ImageService( + self._image = ipyleaflet.ImageService( name=self._variable, crs=self.crs, interactive=True, @@ -683,10 +688,10 @@ def boundary_change(self, change): endpoint='local') # add click handler for popups if self.enable_popups: - self.image.on_click(self.handle_click) + self._image.on_click(self.handle_click) # set the image url self.set_image_url() - self.add(self.image) + self.add(self._image) def __init__(self, ds): # initialize map @@ -701,14 +706,14 @@ def __init__(self, ds): self._ds_selected = None self._variable = None # initialize image and colorbars - self.image = None + self._image = None self.cmap = None self.norm = None self.opacity = None - self.colorbar = None + self._colorbar = None # initialize attributes for popup self.enable_popups = False - self.popup = None + self._popup = None self._data = None self._units = None @@ -767,22 +772,11 @@ def plot(self, m, **kwargs): self._ds_selected = self._ds[self._variable].sel(band=1) else: self._ds_selected = self._ds[self._variable] - # set colorbar limits to 2-98 percentile - # if not using a defined plot range - clim = self._ds_selected.chunk(dict(y=-1,x=-1)).quantile((0.02, 0.98)).values - fmin = np.finfo(np.float64).min - if (kwargs['vmin'] is None) or np.isclose(kwargs['vmin'], fmin): - vmin = clim[0] - else: - vmin = np.copy(kwargs['vmin']) - fmax = np.finfo(np.float64).max - if (kwargs['vmax'] is None) or np.isclose(kwargs['vmax'], fmax): - vmax = clim[-1] - else: - vmax = np.copy(kwargs['vmax']) + # get the normalization bounds + self.get_norm_bounds(**kwargs) # create matplotlib normalization if kwargs['norm'] is None: - self.norm = colors.Normalize(vmin=vmin, vmax=vmax, clip=True) + self.norm = colors.Normalize(vmin=self.vmin, vmax=self.vmax, clip=True) else: self.norm = copy.copy(kwargs['norm']) # get colormap @@ -791,7 +785,7 @@ def plot(self, m, **kwargs): self.opacity = float(kwargs['opacity']) # wait for changes asyncio.ensure_future(self.async_wait_for_bounds()) - self.image = ipyleaflet.ImageService( + self._image = ipyleaflet.ImageService( name=self._variable, crs=self.crs, interactive=True, @@ -799,19 +793,21 @@ def plot(self, m, **kwargs): endpoint='local') # add click handler for popups if self.enable_popups: - self.image.on_click(self.handle_click) + self._image.on_click(self.handle_click) # set the image url self.set_image_url() # add image object to map - self.add(self.image) + self.add(self._image) # add colorbar - if kwargs['colorbar']: + self.colorbar = kwargs['colorbar'] + self.colorbar_position = kwargs['position'] + if self.colorbar: self.add_colorbar( label=self._variable, cmap=self.cmap, opacity=self.opacity, norm=self.norm, - position=kwargs['position'] + position=self.colorbar_position ) def wait_for_change(self, widget, value): @@ -883,9 +879,9 @@ def reset(self): (control.widget._model_name == 'ImageModel'): self.remove(control) # reset layers and controls - self.image = None - self.popup = None - self.colorbar = None + self._image = None + self._popup = None + self._colorbar = None # get map bounding box in projected coordinates def get_bbox(self): @@ -938,6 +934,53 @@ def get_crs(self): return # raise exception raise Exception('Unknown coordinate reference system') + + def get_norm_bounds(self, **kwargs): + """ + Get the colorbar normalization bounds + + Parameters + ---------- + vmin : float or NoneType + Minimum value for normalization + vmax : float or NoneType + Maximum value for normalization + """ + # set default keyword arguments + kwargs.setdefault('vmin', None) + kwargs.setdefault('vmax', None) + # set colorbar limits to 2-98 percentile + # if not using a defined plot range + clim = self._ds_selected.chunk(dict(y=-1,x=-1)).quantile((0.02, 0.98)).values + # set minimum for normalization + fmin = np.finfo(np.float64).min + if (kwargs['vmin'] is None) or np.isclose(kwargs['vmin'], fmin): + self.vmin = clim[0] + self._dynamic = True + else: + self.vmin = np.copy(kwargs['vmin']) + self._dynamic = False + # set maximum for normalization + fmax = np.finfo(np.float64).max + if (kwargs['vmax'] is None) or np.isclose(kwargs['vmax'], fmax): + self.vmax = clim[-1] + self._dynamic = True + else: + self.vmax = np.copy(kwargs['vmax']) + self._dynamic = False + + def validate_norm(self): + """ + Validate the colorbar normalization bounds + """ + fmin = np.finfo(np.float64).min + fmax = np.finfo(np.float64).max + if np.isclose(self.vmin, fmin): + self.vmin = -5 + self._dynamic = False + if np.isclose(self.vmax, fmax): + self.vmax = 5 + self._dynamic = False def clip_image(self, ds): """clip or warp xarray image to bounds of leaflet map @@ -1030,7 +1073,98 @@ def set_image_url(self, *args, **kwargs): """ self.get_bounds() self.get_image_url() - self.image.url = self.url + self._image.url = self.url + + def redraw(self, *args, **kwargs): + """ + Redraw the image on the map + """ + # try to update the selected dataset + try: + self.get_image_url() + except Exception as exc: + pass + else: + # update image url + self._image.url = self.url + # force redrawing of map by removing and adding layer + self.remove(self._image) + self.add(self._image) + + def redraw_colorbar(self, *args, **kwargs): + """ + Redraw the colorbar on the map + """ + try: + if self.colorbar: + self.add_colorbar( + label=self._variable, + cmap=self.cmap, + opacity=self.opacity, + norm=self.norm, + position=self.colorbar_position + ) + except Exception as exc: + pass + + # observe changes in widget parameters + def observe_widget(self, widget, **kwargs): + """observe changes in widget parameters + """ + # connect variable widget to set function + try: + widget.variable.observe(self.set_variable) + except AttributeError: + pass + # connect time lag widget to time slice function + try: + widget.timelag.observe(self.set_lag) + except AttributeError: + pass + # connect normalization widget to set function + try: + widget.range.observe(self.set_norm) + except AttributeError: + pass + # connect dynamic normalization widget to set function + try: + widget.dynamic.observe(self.set_dynamic) + except AttributeError: + pass + # connect colormap widget to set function + try: + widget.cmap.observe(self.set_colormap) + except AttributeError: + pass + # connect reverse colormap widget to set function + try: + widget.reverse.observe(self.set_colormap) + except AttributeError: + pass + + def set_variable(self, sender): + """update the dataframe variable for a new selected variable + """ + # only update variable if a new final + if isinstance(sender['new'], str): + self._variable = sender['new'] + else: + return + # reduce to variable and lag + if (self._ds[self._variable].ndim == 3) and ('time' in self._ds[self._variable].dims): + self._ds_selected = self._ds[self._variable].sel(time=self._ds.time[self.lag]) + elif (self._ds[self._variable].ndim == 3) and ('band' in self._ds[self._variable].dims): + self._ds_selected = self._ds[self._variable].sel(band=1) + else: + self._ds_selected = self._ds[self._variable] + # check if dynamic normalization is enabled + if self._dynamic: + self.get_norm_bounds() + self.norm.vmin = self.vmin + self.norm.vmax = self.vmax + # try to redraw the selected dataset + self.redraw() + self.redraw_colorbar() def set_lag(self, sender): """update the time lag for the selected variable @@ -1041,17 +1175,70 @@ def set_lag(self, sender): else: return # try to update the selected dataset - try: - self._ds_selected = self._ds[self._variable].sel(time=self._ds.time[self.lag]) - self.get_image_url() - except Exception as exc: - pass + self._ds_selected = self._ds[self._variable].sel(time=self._ds.time[self.lag]) + # check if dynamic normalization is enabled + if self._dynamic: + self.get_norm_bounds() + self.norm.vmin = self.vmin + self.norm.vmax = self.vmax + # try to redraw the selected dataset + self.redraw() + if self._dynamic: + self.redraw_colorbar() + + def set_dynamic(self, sender): + """set dynamic normalization for the selected variable + """ + # only update dynamic norm if a new final + if isinstance(sender['new'], bool) and sender['new']: + self.get_norm_bounds() + self._dynamic = True + elif isinstance(sender['new'], bool): + self.vmin, self.vmax = (-5, 5) + self._dynamic = False else: - # update image url - self.image.url = self.url - # force redrawing of map by removing and adding layer - self.remove(self.image) - self.add(self.image) + return + # set the normalization bounds + self.validate_norm() + self.norm.vmin = self.vmin + self.norm.vmax = self.vmax + # try to redraw the selected dataset + self.redraw() + self.redraw_colorbar() + + def set_norm(self, sender): + """update the normalization for the selected variable + """ + # only update norm if a new final + if isinstance(sender['new'], (tuple, list)): + self.vmin, self.vmax = sender['new'] + else: + return + # set the normalization bounds + self.validate_norm() + self.norm.vmin = self.vmin + self.norm.vmax = self.vmax + # try to redraw the selected dataset + self.redraw() + self.redraw_colorbar() + + def set_colormap(self, sender): + """update the colormap for the selected variable + """ + # only update colormap if a new final + if isinstance(sender['new'], str): + cmap_name = self.cmap.name + cmap_reverse_flag = '_r' if cmap_name.endswith('_r') else '' + self.cmap = cm.get_cmap(sender['new'] + cmap_reverse_flag) + elif isinstance(sender['new'], bool): + cmap_name = self.cmap.name.strip('_r') + cmap_reverse_flag = '_r' if sender['new'] else '' + self.cmap = cm.get_cmap(cmap_name + cmap_reverse_flag) + else: + return + # try to redraw the selected dataset + self.redraw() + self.redraw_colorbar() # functional calls for click events def handle_click(self, **kwargs): @@ -1059,8 +1246,8 @@ def handle_click(self, **kwargs): """ lat, lon = kwargs.get('coordinates') # remove any prior instances of popup - if self.popup is not None: - self.remove(self.popup) + if self._popup is not None: + self.remove(self._popup) # attempt to get the coordinate reference system of the dataset try: grid_mapping = self._ds[self._variable].attrs['grid_mapping'] @@ -1080,9 +1267,9 @@ def handle_click(self, **kwargs): # create contextual popup child = ipywidgets.HTML() child.value = '{0:0.1f} {1}'.format(np.squeeze(self._data), self._units) - self.popup = ipyleaflet.Popup(location=(lat, lon), + self._popup = ipyleaflet.Popup(location=(lat, lon), child=child, name='popup') - self.add(self.popup) + self.add(self._popup) # add colorbar widget to leaflet map def add_colorbar(self, **kwargs): @@ -1102,14 +1289,14 @@ def add_colorbar(self, **kwargs): kwargs.setdefault('cmap', 'viridis') kwargs.setdefault('norm', None) kwargs.setdefault('opacity', 1.0) - kwargs.setdefault('orientation', 'horizontal') + kwargs.setdefault('orientation', 'vertical') kwargs.setdefault('label', 'delta_h') kwargs.setdefault('position', 'topright') - kwargs.setdefault('width', 6.0) - kwargs.setdefault('height', 0.4) + kwargs.setdefault('width', 0.2) + kwargs.setdefault('height', 3.0) # remove any prior instances of a colorbar - if self.colorbar is not None: - self.remove(self.colorbar) + if self._colorbar is not None: + self.remove(self._colorbar) # create matplotlib colorbar _, ax = plt.subplots(figsize=(kwargs['width'], kwargs['height'])) cbar = matplotlib.colorbar.ColorbarBase(ax, @@ -1122,14 +1309,15 @@ def add_colorbar(self, **kwargs): cbar.ax.tick_params(which='both', width=1, direction='in') # save colorbar to in-memory png object png = io.BytesIO() - plt.savefig(png, bbox_inches='tight', format='png', transparent=True) + plt.savefig(png, bbox_inches='tight', pad_inches=0.075, + format='png', transparent=True) png.seek(0) # create output widget output = ipywidgets.Image(value=png.getvalue(), format='png') - self.colorbar = ipyleaflet.WidgetControl(widget=output, - transparent_bg=True, position=kwargs['position']) + self._colorbar = ipyleaflet.WidgetControl(widget=output, + transparent_bg=False, position=kwargs['position']) # add colorbar - self.add(self.colorbar) + self.add(self._colorbar) plt.close() # save the current map as an image diff --git a/IS2view/tools.py b/IS2view/tools.py index 8e2c89a..f493d06 100644 --- a/IS2view/tools.py +++ b/IS2view/tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python u""" tools.py -Written by Tyler Sutterley (11/2023) +Written by Tyler Sutterley (04/2024) User interface tools for Jupyter Notebooks PYTHON DEPENDENCIES: @@ -13,8 +13,11 @@ matplotlib: Python 2D plotting library http://matplotlib.org/ https://github.com/matplotlib/matplotlib + panel: Powerful Data Exploration & Web App Framework for Python + https://panel.holoviz.org/index.html UPDATE HISTORY: + Updated 04/2024: include panel import to create a row of widgets Updated 11/2023: set time steps using decimal years rather than lags setting dynamic colormap with float64 min and max Updated 08/2023: added options for ATL14/15 Release-03 data @@ -34,6 +37,12 @@ import ipywidgets except (AttributeError, ImportError, ModuleNotFoundError) as exc: logging.debug("ipywidgets not available") +try: + import panel as pn +except (AttributeError, ImportError, ModuleNotFoundError) as exc: + logging.debug("panel not available") +else: + pn.extension('ipywidgets') try: import matplotlib.cm as cm except (AttributeError, ImportError, ModuleNotFoundError) as exc: @@ -55,6 +64,9 @@ def __init__(self, **kwargs): # pass through some ipywidgets objects self.HBox = ipywidgets.HBox self.VBox = ipywidgets.VBox + # pass through some panel objects + self.Row = pn.Row + self.Column = pn.Column # dropdown menu for setting asset asset_list = ['nsidc-https', 'nsidc-s3', 'atlas-s3', 'atlas-local'] diff --git a/README.rst b/README.rst index fed1d95..78542c5 100644 --- a/README.rst +++ b/README.rst @@ -40,6 +40,7 @@ Dependencies - `ipyleaflet: Interactive maps in the Jupyter notebook `_ - `matplotlib: Python 2D plotting library `_ - `numpy: Scientific Computing Tools For Python `_ +- `panel: Powerful Data Exploration & Web App Framework for Python `_ - `rasterio: Access to geospatial raster data `_ - `rioxarray: geospatial xarray extension powered by rasterio `_ - `setuptools_scm: manager for python package versions using scm metadata `_ diff --git a/doc/source/getting_started/Citations.rst b/doc/source/getting_started/Citations.rst index 39eb972..01b588c 100644 --- a/doc/source/getting_started/Citations.rst +++ b/doc/source/getting_started/Citations.rst @@ -46,6 +46,7 @@ This software is also dependent on other commonly used Python packages: - `matplotlib: Python 2D plotting library `_ - `h5netcdf: Pythonic interface to netCDF4 via h5py `_ - `numpy: Scientific Computing Tools For Python `_ +- `panel: Powerful Data Exploration & Web App Framework for Python `_ - `rasterio: Access to geospatial raster data `_ - `rioxarray: geospatial xarray extension powered by rasterio `_ - `setuptools_scm: manager for python package versions using scm metadata `_ diff --git a/environment.yml b/environment.yml index 8e38147..63fd4f3 100644 --- a/environment.yml +++ b/environment.yml @@ -7,6 +7,7 @@ dependencies: - ipywidgets - matplotlib - notebook + - panel - pip - python>=3.6 - numpy>=1.21 diff --git a/notebooks/IS2-ATL14-Viewer.ipynb b/notebooks/IS2-ATL14-Viewer.ipynb index f4c8150..600104b 100644 --- a/notebooks/IS2-ATL14-Viewer.ipynb +++ b/notebooks/IS2-ATL14-Viewer.ipynb @@ -50,35 +50,6 @@ "])" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "tags": [] - }, - "source": [ - "### Interactive Mapping with Leaflet\n", - "\n", - "Interactive maps within IS2view are built upon [ipyleaflet](https://ipyleaflet.readthedocs.io). Clicking and dragging will pan the field of view, and zooming will adjust the field of view. There are 2 polar stereographic projections available for mapping in IS2view ([North](https://epsg.io/3413) and [South](https://epsg.io/3031)). The map projection, map center and zoom level will all be set based on the ATL14 region selected. The available basemaps are NASA's Next Generation Blue Marble visualization for the Arctic and Antarctic regions.\n", - "\n", - "Transects can be extracted by interactively drawing polylines on the leaflet map or programmatically using [geopandas GeoDataFrames](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.html)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m = IS2view.Leaflet(IS2widgets.projection,\n", - " center=IS2widgets.center,\n", - " zoom=IS2widgets.zoom,\n", - " draw_control=True,\n", - " draw_tools='polyline',\n", - " attribution=True)\n", - "m.map" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -130,8 +101,16 @@ { "attachments": {}, "cell_type": "markdown", - "metadata": {}, + "metadata": { + "tags": [] + }, "source": [ + "### Interactive Mapping with Leaflet\n", + "\n", + "Interactive maps within IS2view are built upon [ipyleaflet](https://ipyleaflet.readthedocs.io). Clicking and dragging will pan the field of view, and zooming will adjust the field of view. There are 2 polar stereographic projections available for mapping in IS2view ([North](https://epsg.io/3413) and [South](https://epsg.io/3031)). The map projection, map center and zoom level will all be set based on the ATL14 region selected. The available basemaps are NASA's Next Generation Blue Marble visualization for the Arctic and Antarctic regions.\n", + "\n", + "Transects can be extracted by interactively drawing polylines on the leaflet map or programmatically using [geopandas GeoDataFrames](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.html). See [Recipes](https://is2view.readthedocs.io/en/latest/user_guide/Recipes.html) for more information.\n", + "\n", "#### Set plot parameters for ATL14" ] }, @@ -141,13 +120,23 @@ "metadata": {}, "outputs": [], "source": [ + "# create leaflet map\n", + "m = IS2view.Leaflet(IS2widgets.projection,\n", + " center=IS2widgets.center,\n", + " zoom=IS2widgets.zoom,\n", + " draw_control=True,\n", + " draw_tools='polyline',\n", + " attribution=False)\n", + "# set plot attributes\n", "IS2widgets.set_variables(ds)\n", "IS2widgets.set_atl14_defaults()\n", - "IS2widgets.VBox([\n", + "widget_pane = IS2widgets.VBox([\n", " IS2widgets.variable,\n", " IS2widgets.cmap,\n", " IS2widgets.reverse,\n", - "])" + "])\n", + "# display as a panel row\n", + "IS2widgets.Row(m.map, widget_pane)" ] }, { @@ -169,7 +158,9 @@ " variable=IS2widgets.variable.value,\n", " cmap=IS2widgets.colormap,\n", " opacity=0.75,\n", - " enable_popups=True)" + " enable_popups=True)\n", + "# observe changes in widget parameters\n", + "ds.leaflet.observe_widget(IS2widgets)" ] }, { @@ -215,7 +206,7 @@ "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" }, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -229,7 +220,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/notebooks/IS2-ATL15-Viewer.ipynb b/notebooks/IS2-ATL15-Viewer.ipynb index c8fcd46..d3e0514 100644 --- a/notebooks/IS2-ATL15-Viewer.ipynb +++ b/notebooks/IS2-ATL15-Viewer.ipynb @@ -54,32 +54,6 @@ "])" ] }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Interactive Mapping with Leaflet\n", - "\n", - "Interactive maps within IS2view are built upon [ipyleaflet](https://ipyleaflet.readthedocs.io). Clicking and dragging will pan the field of view, and zooming will adjust the field of view. There are 2 polar stereographic projections available for mapping in IS2view ([North](https://epsg.io/3413) and [South](https://epsg.io/3031)). The map projection, map center and zoom level will all be set based on the ATL15 region selected. The available basemaps are NASA's Next Generation Blue Marble visualization for the Arctic and Antarctic regions.\n", - "\n", - "Regional time series can be extracted from ATL15 by interactively drawing geometries on the leaflet map or programmatically using [geopandas GeoDataFrames](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.html)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m = IS2view.Leaflet(IS2widgets.projection,\n", - " center=IS2widgets.center,\n", - " zoom=IS2widgets.zoom,\n", - " draw_control=True,\n", - " attribution=True)\n", - "m.map" - ] - }, { "attachments": {}, "cell_type": "markdown", @@ -132,6 +106,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ + "### Interactive Mapping with Leaflet\n", + "\n", + "Interactive maps within IS2view are built upon [ipyleaflet](https://ipyleaflet.readthedocs.io). Clicking and dragging will pan the field of view, and zooming will adjust the field of view. There are 2 polar stereographic projections available for mapping in IS2view ([North](https://epsg.io/3413) and [South](https://epsg.io/3031)). The map projection, map center and zoom level will all be set based on the ATL15 region selected. The available basemaps are NASA's Next Generation Blue Marble visualization for the Arctic and Antarctic regions.\n", + "\n", + "Regional time series can be extracted from ATL15 by interactively drawing geometries on the leaflet map or programmatically using [geopandas GeoDataFrames](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.html). See [Recipes](https://is2view.readthedocs.io/en/latest/user_guide/Recipes.html) for more information.\n", + "\n", "#### Set plot parameters for ATL15\n", "Specifies the variable to plot, the [colormap](https://matplotlib.org/gallery/color/colormap_reference.html), and the normalization for the plot colors." ] @@ -142,17 +122,26 @@ "metadata": {}, "outputs": [], "source": [ + "# create leaflet map\n", + "m = IS2view.Leaflet(IS2widgets.projection,\n", + " center=IS2widgets.center,\n", + " zoom=IS2widgets.zoom,\n", + " draw_control=True,\n", + " attribution=False)\n", + "# set plot attributes\n", "IS2widgets.set_variables(ds)\n", "IS2widgets.set_atl15_defaults()\n", "IS2widgets.set_time_steps(ds)\n", - "IS2widgets.VBox([\n", + "widget_pane = IS2widgets.VBox([\n", " IS2widgets.variable,\n", " IS2widgets.timestep,\n", " IS2widgets.dynamic,\n", " IS2widgets.range,\n", " IS2widgets.cmap,\n", " IS2widgets.reverse,\n", - "])" + "])\n", + "# display as a panel row\n", + "IS2widgets.Row(m.map, widget_pane)" ] }, { @@ -175,7 +164,8 @@ " cmap=IS2widgets.colormap,\n", " opacity=0.75,\n", " enable_popups=False)\n", - "IS2widgets.timelag.observe(ds.leaflet.set_lag)" + "# observe changes in widget parameters\n", + "ds.leaflet.observe_widget(IS2widgets)" ] }, { @@ -224,7 +214,7 @@ "hash": "31f2aee4e71d21fbe5cf8b01ff0e069b9275f58929596ceb00d14d90e3e16cd6" }, "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -238,7 +228,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/requirements.txt b/requirements.txt index b38ec82..fe12063 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ h5netcdf ipyleaflet matplotlib numpy +panel rioxarray setuptools_scm xarray From 960ba57ee306978f006afbb6b9aa872d89bd3727 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Tue, 9 Apr 2024 08:40:03 -0700 Subject: [PATCH 2/4] refactor: just use hbox --- IS2view/tools.py | 12 ------------ environment.yml | 1 - notebooks/IS2-ATL14-Viewer.ipynb | 6 +++--- notebooks/IS2-ATL15-Viewer.ipynb | 6 +++--- requirements.txt | 1 - 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/IS2view/tools.py b/IS2view/tools.py index f493d06..ef6954e 100644 --- a/IS2view/tools.py +++ b/IS2view/tools.py @@ -13,11 +13,8 @@ matplotlib: Python 2D plotting library http://matplotlib.org/ https://github.com/matplotlib/matplotlib - panel: Powerful Data Exploration & Web App Framework for Python - https://panel.holoviz.org/index.html UPDATE HISTORY: - Updated 04/2024: include panel import to create a row of widgets Updated 11/2023: set time steps using decimal years rather than lags setting dynamic colormap with float64 min and max Updated 08/2023: added options for ATL14/15 Release-03 data @@ -37,12 +34,6 @@ import ipywidgets except (AttributeError, ImportError, ModuleNotFoundError) as exc: logging.debug("ipywidgets not available") -try: - import panel as pn -except (AttributeError, ImportError, ModuleNotFoundError) as exc: - logging.debug("panel not available") -else: - pn.extension('ipywidgets') try: import matplotlib.cm as cm except (AttributeError, ImportError, ModuleNotFoundError) as exc: @@ -64,9 +55,6 @@ def __init__(self, **kwargs): # pass through some ipywidgets objects self.HBox = ipywidgets.HBox self.VBox = ipywidgets.VBox - # pass through some panel objects - self.Row = pn.Row - self.Column = pn.Column # dropdown menu for setting asset asset_list = ['nsidc-https', 'nsidc-s3', 'atlas-s3', 'atlas-local'] diff --git a/environment.yml b/environment.yml index 63fd4f3..8e38147 100644 --- a/environment.yml +++ b/environment.yml @@ -7,7 +7,6 @@ dependencies: - ipywidgets - matplotlib - notebook - - panel - pip - python>=3.6 - numpy>=1.21 diff --git a/notebooks/IS2-ATL14-Viewer.ipynb b/notebooks/IS2-ATL14-Viewer.ipynb index 600104b..f3241e4 100644 --- a/notebooks/IS2-ATL14-Viewer.ipynb +++ b/notebooks/IS2-ATL14-Viewer.ipynb @@ -130,13 +130,13 @@ "# set plot attributes\n", "IS2widgets.set_variables(ds)\n", "IS2widgets.set_atl14_defaults()\n", - "widget_pane = IS2widgets.VBox([\n", + "wbox = IS2widgets.VBox([\n", " IS2widgets.variable,\n", " IS2widgets.cmap,\n", " IS2widgets.reverse,\n", "])\n", - "# display as a panel row\n", - "IS2widgets.Row(m.map, widget_pane)" + "# display as a horizontal row\n", + "IS2widgets.HBox([m.map, wbox])" ] }, { diff --git a/notebooks/IS2-ATL15-Viewer.ipynb b/notebooks/IS2-ATL15-Viewer.ipynb index d3e0514..41a7e72 100644 --- a/notebooks/IS2-ATL15-Viewer.ipynb +++ b/notebooks/IS2-ATL15-Viewer.ipynb @@ -132,7 +132,7 @@ "IS2widgets.set_variables(ds)\n", "IS2widgets.set_atl15_defaults()\n", "IS2widgets.set_time_steps(ds)\n", - "widget_pane = IS2widgets.VBox([\n", + "wbox = IS2widgets.VBox([\n", " IS2widgets.variable,\n", " IS2widgets.timestep,\n", " IS2widgets.dynamic,\n", @@ -140,8 +140,8 @@ " IS2widgets.cmap,\n", " IS2widgets.reverse,\n", "])\n", - "# display as a panel row\n", - "IS2widgets.Row(m.map, widget_pane)" + "# display as a horizontal row\n", + "IS2widgets.HBox([m.map, wbox])" ] }, { diff --git a/requirements.txt b/requirements.txt index fe12063..b38ec82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ h5netcdf ipyleaflet matplotlib numpy -panel rioxarray setuptools_scm xarray From 8d35ff282d22c7d8fcf868859ee98bc1b7445b95 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Tue, 9 Apr 2024 08:41:16 -0700 Subject: [PATCH 3/4] fix: scrub panel from docs --- IS2view/tools.py | 2 +- README.rst | 1 - doc/source/getting_started/Citations.rst | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/IS2view/tools.py b/IS2view/tools.py index ef6954e..a1b27b8 100644 --- a/IS2view/tools.py +++ b/IS2view/tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python u""" tools.py -Written by Tyler Sutterley (04/2024) +Written by Tyler Sutterley (03/2023) User interface tools for Jupyter Notebooks PYTHON DEPENDENCIES: diff --git a/README.rst b/README.rst index 78542c5..fed1d95 100644 --- a/README.rst +++ b/README.rst @@ -40,7 +40,6 @@ Dependencies - `ipyleaflet: Interactive maps in the Jupyter notebook `_ - `matplotlib: Python 2D plotting library `_ - `numpy: Scientific Computing Tools For Python `_ -- `panel: Powerful Data Exploration & Web App Framework for Python `_ - `rasterio: Access to geospatial raster data `_ - `rioxarray: geospatial xarray extension powered by rasterio `_ - `setuptools_scm: manager for python package versions using scm metadata `_ diff --git a/doc/source/getting_started/Citations.rst b/doc/source/getting_started/Citations.rst index 01b588c..39eb972 100644 --- a/doc/source/getting_started/Citations.rst +++ b/doc/source/getting_started/Citations.rst @@ -46,7 +46,6 @@ This software is also dependent on other commonly used Python packages: - `matplotlib: Python 2D plotting library `_ - `h5netcdf: Pythonic interface to netCDF4 via h5py `_ - `numpy: Scientific Computing Tools For Python `_ -- `panel: Powerful Data Exploration & Web App Framework for Python `_ - `rasterio: Access to geospatial raster data `_ - `rioxarray: geospatial xarray extension powered by rasterio `_ - `setuptools_scm: manager for python package versions using scm metadata `_ From afdd77adc3bd1073bbdb9ec253948722e4813ca3 Mon Sep 17 00:00:00 2001 From: tsutterley Date: Tue, 9 Apr 2024 08:41:45 -0700 Subject: [PATCH 4/4] Update tools.py --- IS2view/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IS2view/tools.py b/IS2view/tools.py index a1b27b8..8e2c89a 100644 --- a/IS2view/tools.py +++ b/IS2view/tools.py @@ -1,7 +1,7 @@ #!/usr/bin/env python u""" tools.py -Written by Tyler Sutterley (03/2023) +Written by Tyler Sutterley (11/2023) User interface tools for Jupyter Notebooks PYTHON DEPENDENCIES: