-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document Panel in-place updates #539
Merged
kylebarron
merged 4 commits into
developmentseed:main
from
MarcSkovMadsen:enhancement/document-panel-inplace-updates
Jun 5, 2024
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,108 +1,232 @@ | ||
# Panel | ||
|
||
[Panel](https://panel.holoviz.org/) is a tool to build interactive web applications and dashboards using Python code. | ||
[Panel](https://panel.holoviz.org/) is a framework for building interactive tools for notebooks, dashboards, and web applications that can run on a server—all using Python. | ||
|
||
Panel [has been reported to work](https://github.com/developmentseed/lonboard/issues/262) with Lonboard. However, it appears that Panel [does not support reactive updates](https://github.com/holoviz/panel/issues/5921) in the same way that [Shiny](./shiny.md) does, so the map will necessarily be recreated from scratch on every update. | ||
## Prerequisites | ||
|
||
## Example | ||
To run the code below, you need to install the following packages: | ||
|
||
This example was written by [@MarcSkovMadsen](https://github.com/MarcSkovMadsen) in [issue #262](https://github.com/developmentseed/lonboard/issues/262). | ||
```bash | ||
pip install panel colorcet ipywidgets_bokeh geopandas palettable lonboard pyogrio watchfiles | ||
``` | ||
|
||
## Tutorial | ||
|
||
In this tutorial, you will learn how to display a `lonboard` `Map` via the [`IPyWidget`](https://panel.holoviz.org/reference/panes/IPyWidget.html) *pane*. | ||
|
||
![Lonboard map displayed in a Panel component](../assets/panel-display-example.png) | ||
|
||
Create a file named `app.py` with the following content: | ||
|
||
```python | ||
import geopandas as gpd | ||
from lonboard import Map, ScatterplotLayer | ||
import panel as pn | ||
|
||
pn.extension("ipywidgets") | ||
|
||
@pn.cache | ||
def get_data(): | ||
return gpd.read_file(gpd.datasets.get_path("naturalearth_cities")) | ||
|
||
gdf = get_data() | ||
layer = ScatterplotLayer.from_geopandas(gdf, radius_min_pixels=2, get_fill_color="red") | ||
cities_map = Map(layer) | ||
|
||
# Fit to the available space | ||
cities_map.layout.height = cities_map.layout.width = "100%" | ||
|
||
pn.Column( | ||
"# Lonboard Map", | ||
pn.pane.IPyWidget(cities_map, height=500, width=1000), | ||
).servable() | ||
``` | ||
|
||
Now run: | ||
|
||
```bash | ||
panel serve app.py --autoreload | ||
``` | ||
|
||
Finally, open [http://localhost:5006](http://localhost:5006) and you should see the `lonboard` map. | ||
|
||
## How to | ||
|
||
### Update the Map In Place | ||
|
||
Panel supports **in-place updates** of `lonboard` `Map`s, allowing you to update details of a map efficiently without redrawing the entire map. This is useful for updating the map based on user interactions or on a schedule. | ||
|
||
![In place update of map in Panel](../assets/panel-update-in-place-example.gif) | ||
|
||
```python | ||
import geopandas as gpd | ||
from lonboard import Map, ScatterplotLayer | ||
import panel as pn | ||
|
||
pn.extension("ipywidgets") | ||
|
||
colors = { | ||
"Red": [200, 0, 0], | ||
"Green": [0, 200, 0], | ||
"Blue": [0, 0, 200], | ||
} | ||
|
||
@pn.cache | ||
def get_data(): | ||
return gpd.read_file(gpd.datasets.get_path("naturalearth_cities")) | ||
|
||
gdf = get_data() | ||
layer = ScatterplotLayer.from_geopandas(gdf, radius_min_pixels=2, get_fill_color="red") | ||
cities_map = Map(layer) | ||
|
||
# Fit to the available space | ||
cities_map.layout.height = cities_map.layout.width = "100%" | ||
|
||
color_input = pn.widgets.Select( | ||
name="Color", options=list(colors.keys()), description="The color of the points" | ||
) | ||
|
||
```py | ||
"""Panel data app based on https://developmentseed.org/lonboard/latest/examples/north-america-roads/""" | ||
# pip install panel colorcet ipywidgets_bokeh geopandas palettable lonboard | ||
@pn.depends(value=color_input, watch=True) | ||
def set_fill_color(value): | ||
cities_map.layers[0].get_fill_color = colors[value] | ||
|
||
pn.Column( | ||
color_input, | ||
pn.pane.IPyWidget(cities_map, height=500, width=1000), | ||
).servable() | ||
``` | ||
|
||
### Build an Application | ||
|
||
With Panel, you can build reusable components and styled web applications in just a few lines of code. | ||
|
||
The example below is based on the [North America Roads](../examples/north-america-roads.ipynb) example. | ||
|
||
![Panel lonboard application](../assets/panel-application-example.gif) | ||
|
||
```python | ||
import colorcet as cc | ||
import geopandas as gpd | ||
import param | ||
|
||
from lonboard import Map, PathLayer | ||
from lonboard.colormap import apply_continuous_cmap | ||
from lonboard._viewport import compute_view | ||
from palettable.palette import Palette | ||
|
||
import panel as pn | ||
|
||
pn.extension("ipywidgets") | ||
|
||
url = "https://naciscdn.org/naturalearth/10m/cultural/ne_10m_roads_north_america.zip" | ||
path = "ne_10m_roads_north_america.zip" | ||
|
||
try: | ||
gdf = pn.state.as_cached( | ||
"ne_10m_roads_north_america", gpd.read_file, filename=path, engine="pyogrio" | ||
) | ||
except: | ||
gdf = pn.state.as_cached( | ||
"ne_10m_roads_north_america", gpd.read_file, filename=url, engine="pyogrio" | ||
) | ||
@pn.cache | ||
def get_data(): | ||
return gpd.read_file(filename=url, engine="pyogrio") | ||
|
||
gdf = get_data() | ||
state_options = sorted(state for state in gdf["state"].unique() if state) | ||
|
||
description = """# Lonboard | ||
|
||
A Python library for **fast, interactive geospatial vector data visualization** in Jupyter (and Panel). | ||
|
||
By utilizing new technologies like `GeoArrow` and `GeoParquet` in conjunction with GPU-based map rendering, Lonboard aims to enable visualizing large geospatial datasets interactively through a simple interface.""" | ||
|
||
logo = pn.pane.Image( | ||
"https://github.com/developmentseed/lonboard/raw/main/assets/dalle-lonboard.jpg" | ||
) | ||
|
||
def to_rgb(hex: str) -> list: | ||
h = hex.strip("#") | ||
return list(int(h[i : i + 2], 16) for i in (0, 2, 4)) | ||
|
||
|
||
def to_palette(cmap) -> Palette: | ||
"""Returns the ColorCet colormap as a palettable Palette""" | ||
colors = [to_rgb(item) for item in cmap] | ||
return Palette(name="colorcet", map_type="colorcet", colors=colors) | ||
|
||
|
||
def create_map(state="California", cmap=cc.fire, alpha=0.8): | ||
palette = to_palette(cmap) | ||
data = gdf[gdf["state"] == state] | ||
layer = PathLayer.from_geopandas(data, width_min_pixels=0.8) | ||
normalized_scale_rank = (data["scalerank"] - 3) / 9 | ||
layer.get_color = apply_continuous_cmap(normalized_scale_rank, palette, alpha=alpha) | ||
map_ = Map(layer, _height=650) | ||
return map_ | ||
|
||
|
||
description = """# lonboard | ||
|
||
A Python library for **fast, interactive geospatial vector data visualization** in Jupyter (and Panel). | ||
|
||
By utilizing new technologies like `GeoArrow` and `GeoParquet` in conjunction with GPU-based map rendering, lonboard aims to enable visualizing large geospatial datasets interactively through a simple interface.""" | ||
|
||
|
||
# THE PANEL APP | ||
pn.extension("ipywidgets") | ||
state = pn.widgets.Select( | ||
value="California", | ||
options=state_options, | ||
width=150, | ||
name="State", | ||
sizing_mode="stretch_width", | ||
) | ||
cmap = pn.widgets.ColorMap( | ||
value=cc.fire, | ||
options=cc.palette, | ||
ncols=3, | ||
swatch_width=100, | ||
name="cmap by Colorcet", | ||
sizing_mode="stretch_width", | ||
) | ||
alpha = pn.widgets.FloatSlider( | ||
value=0.8, start=0, end=1, name="Alpha", min_width=100, sizing_mode="stretch_width" | ||
) | ||
logo = pn.pane.Image( | ||
"https://github.com/developmentseed/lonboard/raw/main/assets/dalle-lonboard.jpg" | ||
) | ||
def title(state): | ||
return f"# North America Roads: {state}" | ||
|
||
settings = pn.Column(state, cmap, alpha) | ||
description = pn.Column(pn.pane.Markdown(description, margin=5), logo) | ||
component = pn.Column( | ||
pn.bind(title, state=state), | ||
pn.panel( | ||
pn.bind(create_map, state=state, cmap=cmap, alpha=alpha.param.value_throttled), | ||
sizing_mode="stretch_both", | ||
), | ||
sizing_mode="stretch_both", | ||
) | ||
class StateViewer(pn.viewable.Viewer): | ||
value: Map = param.ClassSelector(class_=Map, doc="The map object", constant=True) | ||
state: str = param.Selector(default="California", objects=state_options) | ||
cmap: str = param.Selector(default=cc.fire, objects=cc.palette, label="cmap by Colorcet") | ||
alpha: float = param.Number(default=0.8, bounds=(0, 1)) | ||
|
||
data = param.DataFrame() | ||
|
||
def __init__(self, **params): | ||
params["value"] = params.get("value", Map(layers=[], view_state={"longitude": -119.81446785010868, "latitude": 36.08305565437565, "zoom": 5})) | ||
|
||
super().__init__(**params) | ||
|
||
self.value.layout.width=self.value.layout.height="100%" | ||
|
||
self.description = pn.Column(pn.pane.Markdown(description, margin=5), logo) | ||
self.settings = pn.Column( | ||
pn.widgets.Select.from_param(self.param.state, sizing_mode="stretch_width"), | ||
pn.widgets.ColorMap.from_param( | ||
self.param.cmap, | ||
ncols=3, | ||
swatch_width=100, | ||
name="cmap by Colorcet", | ||
sizing_mode="stretch_width", | ||
), | ||
pn.widgets.FloatSlider.from_param( | ||
self.param.alpha, sizing_mode="stretch_width" | ||
), | ||
margin=5, | ||
sizing_mode="fixed", | ||
width=300, | ||
) | ||
self.view = pn.Column( | ||
self._title, pn.pane.IPyWidget(self.value, sizing_mode="stretch_both") | ||
) | ||
self._layout = pn.Row( | ||
pn.Column(self.settings, sizing_mode="fixed", width=300), | ||
self.view, | ||
sizing_mode="stretch_both", | ||
) | ||
|
||
def __panel__(self): | ||
return self._layout | ||
|
||
@param.depends("state", watch=True, on_init=True) | ||
def _update_data(self): | ||
self.data = gdf[gdf["state"] == self.state] | ||
|
||
def _get_color(self): | ||
palette = to_palette(self.cmap) | ||
normalized_scale_rank = (self.data["scalerank"] - 3) / 9 | ||
return apply_continuous_cmap(normalized_scale_rank, palette, alpha=self.alpha) | ||
|
||
@param.depends("data", watch=True) | ||
def _update_value(self): | ||
layer = PathLayer.from_geopandas(self.data, width_min_pixels=0.8) | ||
layer.get_color = self._get_color() | ||
self.value.layers = [layer] | ||
self._fly_to_center() | ||
|
||
def _fly_to_center(self): | ||
computed_view_state = compute_view(self.value.layers) | ||
self.value.fly_to( | ||
**computed_view_state, | ||
duration=1000, | ||
) | ||
|
||
@param.depends("cmap", "alpha", watch=True) | ||
def _update_layer_get_color(self): | ||
self.value.layers[0].get_color = self._get_color() | ||
|
||
@param.depends("state") | ||
def _title(self): | ||
return f"# North America Roads: {self.state}" | ||
|
||
viewer = StateViewer() | ||
pn.template.FastListTemplate( | ||
logo="https://panel.holoviz.org/_static/logo_horizontal_dark_theme.png", | ||
title="Works with LonBoard", | ||
main=[component], | ||
sidebar=[description, settings], | ||
title="Works with Lonboard", | ||
sidebar=[viewer.description, viewer.settings], | ||
main=[viewer.view], | ||
main_layout=None, | ||
).servable() | ||
``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh cool. TIL. Maybe we should be setting this from the Lonboard side.