posit-dev / py-shinywidgets

Render ipywidgets inside a PyShiny app
MIT License
41 stars 2 forks source link

Using `@render_widget` with non-UI ipywidgets #133

Closed kylebarron closed 4 months ago

kylebarron commented 4 months ago

Description

I develop a Jupyter Widget called lonboard for geospatial data visualization. It uses the same underlying JS library as pydeck but is 10-50x faster/more capable because it uses efficient binary data serialization instead of JSON and because it can copy binary buffers directly to the GPU.

I'm trying to add an example in the lonboard documentation of how to use with shiny, but it's unclear how to handle reactivity of multiple widgets, where only one is intended to be rendered.

The architecture of lonboard is that there's one top-level Map widget but a variety of Layer classes to render points, lines, polygons etc. Each of these Layer classes is themselves a Widget so that their attributes can be reactive. But only the Map is associated with a JS view.

I'd like to have each widget as its own reactive element.

Describe what you were trying to get done. Tell us what happened, what went wrong, and what you expected to happen.

What I Did

This first example worked but is a bit clunky

import geopandas as gpd
from shiny import reactive
from shiny.express import input, ui
from shinywidgets import render_widget

from lonboard import Map, ScatterplotLayer

colors = {
    "Red": [200, 0, 0],
    "Green": [0, 200, 0],
    "Blue": [0, 0, 200],
}

ui.input_select("color_select", "Color", choices=list(colors.keys()))

@render_widget
def map():
    gdf = gpd.read_file(gpd.datasets.get_path("naturalearth_cities"))
    layer = ScatterplotLayer.from_geopandas(gdf, radius_min_pixels=2)
    return Map(layer)

@reactive.effect
def set_fill_color():
    map.widget.layers[0].get_fill_color = colors[input.color_select()]

https://github.com/posit-dev/py-shinywidgets/assets/15164633/b008703f-4328-4e9d-940d-fe4324457cd1

Setting map.widget.layers[0].get_fill_color is pretty clunky because you're accessing the layer widget through the Map object. Instead, the usual suggestion in the docs in Jupyter is to edit layer.get_fill_color directly. So what I was hoping would work is the following:

import geopandas as gpd
from shiny import reactive
from shiny.express import input, ui
from shinywidgets import render_widget

from lonboard import Map, ScatterplotLayer

colors = {
    "Red": [200, 0, 0],
    "Green": [0, 200, 0],
    "Blue": [0, 0, 200],
}

ui.input_select("color_select", "Color", choices=list(colors.keys()))

@render_widget
def layer():
    gdf = gpd.read_file(gpd.datasets.get_path("naturalearth_cities"))
    return ScatterplotLayer.from_geopandas(gdf, radius_min_pixels=2)

@render_widget
def map():
    return Map(layer.widget)

@reactive.effect
def set_fill_color():
    layer.widget.get_fill_color = colors[input.color_select()]

Here there are two widgets, the layer and the map. This is simpler because set_fill_color is reactive onto the layer instead of the map. But nothing renders on screen. Is there a different decorator I should be using instead of render_widget on def layer?

image
cpsievert commented 4 months ago

Hi @kylebarron, thanks for this issue! I hadn't really anticipated wanting to use @render_widget in this way (with Widget that isn't a non-DOMWidget), but that makes total sense, and fortunately making this work was fairly straight-forward -- #134 will address it.

Also, to get your examples working, I had to change Map(layer) to Map(layers=[layer]) -- should that be necessary?

kylebarron commented 4 months ago

Hi @kylebarron, thanks for this issue! I hadn't really anticipated wanting to use @render_widget in this way (with Widget that isn't a non-DOMWidget), but that makes total sense, and fortunately making this work was fairly straight-forward -- #134 will address it.

Awesome, thanks! Looking forward to trying this out!

Also, to get your examples working, I had to change Map(layer) to Map(layers=[layer]) -- should that be necessary?

I was using the local main branch of lonboard; previously layers was only a keyword argument but the upcoming 0.6 release (hopefully within a couple days) allows broader input for simplicity.