Skip to content

Commit

Permalink
render_widget now attaches it's return value (and as_widget() equival…
Browse files Browse the repository at this point in the history
…ent) to the decorated function
  • Loading branch information
cpsievert committed Nov 20, 2023
1 parent 57e25d7 commit 96d88fd
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 93 deletions.
40 changes: 22 additions & 18 deletions examples/ipyleaflet/app.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,50 @@
import ipyleaflet as L
from htmltools import css
from shiny import *
from shiny import App, reactive, render, req, ui

from shinywidgets import output_widget, reactive_read, register_widget
from shinywidgets import output_widget, reactive_read, render_widget

app_ui = ui.page_fillable(
ui.div(
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10),
),
ui.card(
ui.output_text("map_bounds"),
style=css(
display="flex", justify_content="center", align_items="center", gap="2rem"
),
fill=False
),
ui.card(
output_widget("lmap")
),
output_widget("map"),
title="ipyleaflet demo"
)


def server(input, output, session):

# Initialize and display when the session starts (1)
map = L.Map(center=(52, 360), zoom=4)
register_widget("map", map)
@output
@render_widget
def lmap():
return L.Map(center=(52, 360), zoom=4)

# When the slider changes, update the map's zoom attribute (2)
@reactive.Effect
def _():
map.zoom = input.zoom()
lmap.widget.zoom = input.zoom()

# When zooming directly on the map, update the slider's value (2 and 3)
@reactive.Effect
def _():
ui.update_slider("zoom", value=reactive_read(map, "zoom"))
zoom = reactive_read(lmap.widget, "zoom")
ui.update_slider("zoom", value=zoom)

# Everytime the map's bounds change, update the output message (3)
@output
@render.text
def map_bounds():
b = reactive_read(map, "bounds")
b = reactive_read(lmap.widget, "bounds")
req(b)
lat = [b[0][0], b[0][1]]
lon = [b[1][0], b[1][1]]
return f"The current latitude is {lat} and longitude is {lon}"
lat = [round(x) for x in [b[0][0], b[0][1]]]
lon = [round(x) for x in [b[1][0], b[1][1]]]
return f"The map bounds is currently {lat} / {lon}"


app = App(app_ui, server)
41 changes: 22 additions & 19 deletions examples/ipywidgets/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,32 @@

from shinywidgets import *

app_ui = ui.page_fluid(output_widget("slider"), ui.output_text("value"))
app_ui = ui.page_fluid(output_widget("slider", fillable=False, fill=False), ui.output_text("slider_val"))


def server(input: Inputs, output: Outputs, session: Session):
s: Widget = ipy.IntSlider(
value=7,
min=0,
max=10,
step=1,
description="Test:",
disabled=False,
continuous_update=False,
orientation="horizontal",
readout=True,
readout_format="d",
)

register_widget("slider", s)

@output(id="value")

@output
@render_widget
def slider():
return ipy.IntSlider(
value=7,
min=0,
max=10,
step=1,
description="Test:",
disabled=False,
continuous_update=False,
orientation="horizontal",
readout=True,
readout_format="d",
)

@output
@render.text
def _():
return f"The value of the slider is: {reactive_read(s, 'value')}"
def slider_val():
val = reactive_read(slider.widget, "value")
return f"The value of the slider is: {val}"


app = App(app_ui, server, debug=True)
46 changes: 24 additions & 22 deletions examples/plotly/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from shiny import *
from sklearn.linear_model import LinearRegression

from shinywidgets import output_widget, register_widget
from shinywidgets import output_widget, render_widget

# Generate some data and fit a linear regression
n = 10000
Expand All @@ -15,35 +15,37 @@

app_ui = ui.page_fillable(
ui.input_checkbox("show_fit", "Show fitted line", value=True),
output_widget("scatterplot"),
output_widget("scatterplot")
)


def server(input, output, session):

scatterplot = go.FigureWidget(
data=[
go.Scattergl(
x=x,
y=y,
mode="markers",
marker=dict(color="rgba(0, 0, 0, 0.05)", size=5),
),
go.Scattergl(
x=xgrid,
y=fit.intercept_ + fit.coef_[0] * xgrid,
mode="lines",
line=dict(color="red", width=2),
),
],
layout={"showlegend": False},
)

register_widget("scatterplot", scatterplot)
@output
@render_widget
def scatterplot():
return go.FigureWidget(
data=[
go.Scattergl(
x=x,
y=y,
mode="markers",
marker=dict(color="rgba(0, 0, 0, 0.05)", size=5),
),
go.Scattergl(
x=xgrid,
y=fit.intercept_ + fit.coef_[0] * xgrid,
mode="lines",
line=dict(color="red", width=2),
),
],
layout={"showlegend": False},
)

@reactive.Effect
def _():
scatterplot.data[1].visible = input.show_fit()
plt.data[1].visible = input.show_fit()



app = App(app_ui, server)
63 changes: 32 additions & 31 deletions examples/pydeck/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,45 @@

app_ui = ui.page_fillable(
ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1),
output_widget("pydeck")
output_widget("deckmap")
)

def server(input: Inputs, output: Outputs, session: Session):

UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"

layer = pdk.Layer(
"HexagonLayer", # `type` positional argument is here
UK_ACCIDENTS_DATA,
get_position=["lng", "lat"],
auto_highlight=True,
elevation_scale=50,
pickable=True,
elevation_range=[0, 3000],
extruded=True,
coverage=1,
)

view_state = pdk.ViewState(
longitude=-1.415,
latitude=52.2323,
zoom=6,
min_zoom=5,
max_zoom=15,
pitch=40.5,
bearing=-27.36,
)

deck = pdk.Deck(layers=[layer], initial_view_state=view_state)

# Register either the deck (or deck_widget) instance
register_widget("pydeck", deck)
@output
@render_widget
def deckmap():

UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"

layer = pdk.Layer(
"HexagonLayer", # `type` positional argument is here
UK_ACCIDENTS_DATA,
get_position=["lng", "lat"],
auto_highlight=True,
elevation_scale=50,
pickable=True,
elevation_range=[0, 3000],
extruded=True,
coverage=1,
)

view_state = pdk.ViewState(
longitude=-1.415,
latitude=52.2323,
zoom=6,
min_zoom=5,
max_zoom=15,
pitch=40.5,
bearing=-27.36,
)

return pdk.Deck(layers=[layer], initial_view_state=view_state)

@reactive.Effect()
def _():
deck.initial_view_state.zoom = input.zoom()
deck.update()
deckmap.value.initial_view_state.zoom = input.zoom()
deckmap.value.update()


app = App(app_ui, server)
31 changes: 28 additions & 3 deletions shinywidgets/_shinywidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from ipywidgets.widgets.widget import (
_remove_buffers, # pyright: ignore[reportUnknownVariableType, reportGeneralTypeIssues]
)
from shiny import Session, reactive
from shiny import Session, reactive, req
from shiny.http_staticfiles import StaticFiles
from shiny.module import resolve_id
from shiny.render.transformer import (
Expand Down Expand Up @@ -216,10 +216,13 @@ async def WidgetTransformer(
_fn: ValueFn[object | None],
) -> dict[str, Any] | None:
value = await resolve_value_fn(_fn)
_fn.value = value # type: ignore
_fn.widget = None # type: ignore
if value is None:
return None
widget = as_widget(value)
widget, fill = set_layout_defaults(widget)
_fn.widget = widget # type: ignore
return {"model_id": widget.model_id, "fill": fill} # type: ignore


Expand All @@ -232,12 +235,34 @@ def render_widget(fn: WidgetTransformer.ValueFn) -> WidgetTransformer.OutputRend
def render_widget() -> WidgetTransformer.OutputRendererDecorator:
...


def render_widget(
fn: WidgetTransformer.ValueFn | None = None,
) -> WidgetTransformer.OutputRenderer | WidgetTransformer.OutputRendererDecorator:
return WidgetTransformer(fn)
res = WidgetTransformer(fn)

# Make the `res._value_fn.widget` attribute that we set in WidgetTransformer
# accessible via `res.widget`
def get_widget(*_: object) -> Optional[Widget]:
w = res._value_fn.widget # type: ignore
if w is None:
req(False)
return None
return w

def set_widget(*_: object):
raise RuntimeError("The widget attribute of a @render_widget function is read only.")

setattr(res.__class__, "widget", property(get_widget, set_widget))

def get_value(*_: object) -> object | None:
return res._value_fn.value # type: ignore

def set_value(*_: object):
raise RuntimeError("The value attribute of a @render_widget function is read only.")

setattr(res.__class__, "value", property(get_value, set_value))

return res

def reactive_read(widget: Widget, names: Union[str, Sequence[str]]) -> Any:
reactive_depend(widget, names)
Expand Down

0 comments on commit 96d88fd

Please sign in to comment.