Skip to content

Commit 4451359

Browse files
cpsievertschloerke
andauthored
render_widget now attaches it's return value to the decorated function (#119)
Co-authored-by: Barret Schloerke <[email protected]>
1 parent e0de2d4 commit 4451359

File tree

15 files changed

+495
-335
lines changed

15 files changed

+495
-335
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [UNRELEASED]
99

10+
* The `@render_widget` decorator now attaches a `widget` (and `value`) attribute to the function it decorates. This allows for easier access to the widget instance (or value), and eliminates the need for `register_widget` (which is now soft deprecated). (#119)
1011
* Reduce default plot margins on plotly graphs.
1112

1213
## [0.2.4] - 2023-11-20

examples/altair/app.py

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,24 @@
11
import altair as alt
2-
from shiny import App, render, ui
2+
import shiny.express
3+
from shiny import render
34
from vega_datasets import data
45

5-
from shinywidgets import output_widget, reactive_read, register_widget
6+
from shinywidgets import reactive_read, render_altair
67

7-
source = data.cars()
88

9-
app_ui = ui.page_fluid(
10-
ui.output_text_verbatim("selection"),
11-
output_widget("chart")
12-
)
9+
# Output selection information (click on legend in the plot)
10+
@render.text
11+
def selection():
12+
pt = reactive_read(jchart.widget.selections, "point")
13+
return "Selected point: " + str(pt)
1314

14-
def server(input, output, session):
15-
16-
# Replicate JupyterChart interactivity
17-
# https://altair-viz.github.io/user_guide/jupyter_chart.html#point-selections
15+
# Replicate JupyterChart interactivity
16+
# https://altair-viz.github.io/user_guide/jupyter_chart.html#point-selections
17+
@render_altair
18+
def jchart():
1819
brush = alt.selection_point(name="point", encodings=["color"], bind="legend")
19-
chart = alt.Chart(source).mark_point().encode(
20+
return alt.Chart(data.cars()).mark_point().encode(
2021
x='Horsepower:Q',
2122
y='Miles_per_Gallon:Q',
2223
color=alt.condition(brush, 'Origin:N', alt.value('grey')),
2324
).add_params(brush)
24-
25-
jchart = alt.JupyterChart(chart)
26-
27-
# Display/register the chart in the app_ui
28-
register_widget("chart", jchart)
29-
30-
# Reactive-ly read point selections
31-
@output
32-
@render.text
33-
def selection():
34-
pt = reactive_read(jchart.selections, "point")
35-
return "Selected point: " + str(pt)
36-
37-
app = App(app_ui, server)

examples/ipyleaflet/app.py

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,36 @@
11
import ipyleaflet as L
2-
from htmltools import css
3-
from shiny import *
4-
5-
from shinywidgets import output_widget, reactive_read, register_widget
6-
7-
app_ui = ui.page_fillable(
8-
ui.div(
9-
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10),
10-
ui.output_text("map_bounds"),
11-
style=css(
12-
display="flex", justify_content="center", align_items="center", gap="2rem"
13-
),
14-
),
15-
output_widget("map"),
16-
)
17-
18-
19-
def server(input, output, session):
20-
21-
# Initialize and display when the session starts (1)
22-
map = L.Map(center=(52, 360), zoom=4)
23-
register_widget("map", map)
24-
25-
# When the slider changes, update the map's zoom attribute (2)
26-
@reactive.Effect
27-
def _():
28-
map.zoom = input.zoom()
29-
30-
# When zooming directly on the map, update the slider's value (2 and 3)
31-
@reactive.Effect
32-
def _():
33-
ui.update_slider("zoom", value=reactive_read(map, "zoom"))
34-
35-
# Everytime the map's bounds change, update the output message (3)
36-
@output
37-
@render.text
38-
def map_bounds():
39-
b = reactive_read(map, "bounds")
40-
req(b)
41-
lat = [b[0][0], b[0][1]]
42-
lon = [b[1][0], b[1][1]]
43-
return f"The current latitude is {lat} and longitude is {lon}"
2+
from shiny import reactive, render, req
3+
from shiny.express import input, ui
4+
5+
from shinywidgets import reactive_read, render_widget
6+
7+
ui.page_opts(title="ipyleaflet demo")
8+
9+
with ui.sidebar():
10+
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10)
4411

12+
@render_widget
13+
def lmap():
14+
return L.Map(center=(52, 360), zoom=4)
4515

46-
app = App(app_ui, server)
16+
# When the slider changes, update the map's zoom attribute
17+
@reactive.Effect
18+
def _():
19+
lmap.widget.zoom = input.zoom()
20+
21+
# When zooming directly on the map, update the slider's value
22+
@reactive.Effect
23+
def _():
24+
zoom = reactive_read(lmap.widget, "zoom")
25+
ui.update_slider("zoom", value=zoom)
26+
27+
28+
with ui.card(fill=False):
29+
# Everytime the map's bounds change, update the output message
30+
@render.ui
31+
def map_bounds():
32+
b = reactive_read(lmap.widget, "bounds")
33+
req(b)
34+
lat = [round(x) for x in [b[0][0], b[0][1]]]
35+
lon = [round(x) for x in [b[1][0], b[1][1]]]
36+
return f"The map bounds is currently {lat} / {lon}"

examples/ipywidgets/app.py

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import ipywidgets as ipy
2-
from ipywidgets.widgets.widget import Widget
3-
from shiny import *
1+
import shiny.express
2+
from ipywidgets import IntSlider
3+
from shiny import render
44

5-
from shinywidgets import *
5+
from shinywidgets import reactive_read, render_widget
66

7-
app_ui = ui.page_fluid(output_widget("slider"), ui.output_text("value"))
87

9-
10-
def server(input: Inputs, output: Outputs, session: Session):
11-
s: Widget = ipy.IntSlider(
8+
@render_widget
9+
def slider():
10+
return IntSlider(
1211
value=7,
1312
min=0,
1413
max=10,
@@ -21,12 +20,7 @@ def server(input: Inputs, output: Outputs, session: Session):
2120
readout_format="d",
2221
)
2322

24-
register_widget("slider", s)
25-
26-
@output(id="value")
27-
@render.text
28-
def _():
29-
return f"The value of the slider is: {reactive_read(s, 'value')}"
30-
31-
32-
app = App(app_ui, server, debug=True)
23+
@render.ui
24+
def slider_val():
25+
val = reactive_read(slider.widget, "value")
26+
return f"The value of the slider is: {val}"

examples/plotly/app.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import numpy as np
22
import plotly.graph_objs as go
3-
from shiny import *
3+
from shiny import reactive
4+
from shiny.express import input, ui
45
from sklearn.linear_model import LinearRegression
56

6-
from shinywidgets import output_widget, register_widget
7+
from shinywidgets import render_plotly
78

89
# Generate some data and fit a linear regression
910
n = 10000
@@ -13,15 +14,13 @@
1314
fit = LinearRegression().fit(x.reshape(-1, 1), dat[1])
1415
xgrid = np.linspace(start=min(x), stop=max(x), num=30)
1516

16-
app_ui = ui.page_fillable(
17-
ui.input_checkbox("show_fit", "Show fitted line", value=True),
18-
output_widget("scatterplot"),
19-
)
17+
ui.page_opts(title="Plotly demo", fillable=True)
2018

19+
ui.input_checkbox("show_fit", "Show fitted line", value=True)
2120

22-
def server(input, output, session):
23-
24-
scatterplot = go.FigureWidget(
21+
@render_plotly
22+
def scatterplot():
23+
return go.FigureWidget(
2524
data=[
2625
go.Scattergl(
2726
x=x,
@@ -39,11 +38,7 @@ def server(input, output, session):
3938
layout={"showlegend": False},
4039
)
4140

42-
register_widget("scatterplot", scatterplot)
43-
44-
@reactive.Effect
45-
def _():
46-
scatterplot.data[1].visible = input.show_fit()
47-
4841

49-
app = App(app_ui, server)
42+
@reactive.Effect
43+
def _():
44+
scatterplot.widget.data[1].visible = input.show_fit()

examples/pydeck/app.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import pydeck as pdk
2-
from shiny import *
2+
from shiny import reactive
3+
from shiny.express import input, ui
34

4-
from shinywidgets import *
5+
from shinywidgets import render_pydeck
56

6-
app_ui = ui.page_fillable(
7-
ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1),
8-
output_widget("pydeck")
9-
)
10-
11-
def server(input: Inputs, output: Outputs, session: Session):
7+
ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1)
128

9+
@render_pydeck
10+
def deckmap():
1311
UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"
14-
1512
layer = pdk.Layer(
16-
"HexagonLayer", # `type` positional argument is here
13+
"HexagonLayer",
1714
UK_ACCIDENTS_DATA,
1815
get_position=["lng", "lat"],
1916
auto_highlight=True,
@@ -23,7 +20,6 @@ def server(input: Inputs, output: Outputs, session: Session):
2320
extruded=True,
2421
coverage=1,
2522
)
26-
2723
view_state = pdk.ViewState(
2824
longitude=-1.415,
2925
latitude=52.2323,
@@ -33,16 +29,9 @@ def server(input: Inputs, output: Outputs, session: Session):
3329
pitch=40.5,
3430
bearing=-27.36,
3531
)
32+
return pdk.Deck(layers=[layer], initial_view_state=view_state)
3633

37-
deck = pdk.Deck(layers=[layer], initial_view_state=view_state)
38-
39-
# Register either the deck (or deck_widget) instance
40-
register_widget("pydeck", deck)
41-
42-
@reactive.Effect()
43-
def _():
44-
deck.initial_view_state.zoom = input.zoom()
45-
deck.update()
46-
47-
48-
app = App(app_ui, server)
34+
@reactive.effect()
35+
def _():
36+
deckmap.value.initial_view_state.zoom = input.zoom()
37+
deckmap.value.update()

setup.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ setup_requires =
3434
install_requires =
3535
ipywidgets>=7.6.5
3636
jupyter_core
37-
shiny>=0.5.1.9003
37+
# shiny>=0.6.1.9003
38+
shiny @ git+https://github.com/posit-dev/py-shiny.git
3839
python-dateutil>=2.8.2
3940
# Needed because of https://github.com/python/importlib_metadata/issues/411
4041
importlib-metadata>=4.8.3,<5; python_version < "3.8"

shinywidgets/__init__.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,32 @@
44
__email__ = "[email protected]"
55
__version__ = "0.2.4.9000"
66

7+
from ._as_widget import as_widget
78
from ._dependencies import bokeh_dependency
8-
from ._shinywidgets import (
9-
as_widget,
10-
output_widget,
11-
reactive_read,
12-
register_widget,
9+
from ._output_widget import output_widget
10+
from ._render_widget import (
11+
render_altair,
12+
render_bokeh,
13+
render_plotly,
14+
render_pydeck,
1315
render_widget,
1416
)
17+
from ._shinywidgets import reactive_read, register_widget
1518

16-
__all__ = ("output_widget", "register_widget", "render_widget", "reactive_read", "bokeh_dependency", "as_widget")
19+
__all__ = (
20+
# Render methods first
21+
"render_widget",
22+
"render_altair",
23+
"render_bokeh",
24+
"render_plotly",
25+
"render_pydeck",
26+
# Reactive read second
27+
"reactive_read",
28+
# UI methods third
29+
"output_widget",
30+
# Other methods last
31+
"as_widget",
32+
"bokeh_dependency",
33+
# Soft deprecated
34+
"register_widget",
35+
)

shinywidgets/_as_widget.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional
22

3-
from ipywidgets.widgets.widget import Widget
3+
from ipywidgets.widgets.widget import Widget # pyright: ignore[reportMissingTypeStubs]
44

55
from ._dependencies import widget_pkg
66

@@ -35,7 +35,7 @@ def as_widget(x: object) -> Widget:
3535

3636
def as_widget_altair(x: object) -> Optional[Widget]:
3737
try:
38-
from altair import JupyterChart
38+
from altair import JupyterChart # pyright: ignore[reportMissingTypeStubs]
3939
except ImportError:
4040
raise RuntimeError(
4141
"Failed to import altair.JupyterChart (do you need to pip install -U altair?)"
@@ -46,7 +46,7 @@ def as_widget_altair(x: object) -> Optional[Widget]:
4646

4747
def as_widget_bokeh(x: object) -> Optional[Widget]:
4848
try:
49-
from jupyter_bokeh import BokehModel
49+
from jupyter_bokeh import BokehModel # pyright: ignore[reportMissingTypeStubs]
5050
except ImportError:
5151
raise ImportError(
5252
"Install the jupyter_bokeh package to use bokeh with shinywidgets."
@@ -55,18 +55,19 @@ def as_widget_bokeh(x: object) -> Optional[Widget]:
5555
# TODO: ideally we'd do this in set_layout_defaults() but doing
5656
# `BokehModel(x)._model.sizing_mode = "stretch_both"`
5757
# there, but that doesn't seem to work??
58-
from bokeh.plotting import figure
59-
if isinstance(x, figure):
60-
x.sizing_mode = "stretch_both"
58+
from bokeh.plotting import figure # pyright: ignore[reportMissingTypeStubs]
59+
60+
if isinstance(x, figure): # type: ignore
61+
x.sizing_mode = "stretch_both" # pyright: ignore[reportGeneralTypeIssues]
6162

6263
return BokehModel(x) # type: ignore
6364

6465

6566
def as_widget_plotly(x: object) -> Optional[Widget]:
6667
# Don't need a try import here since this won't be called unless x is a plotly object
67-
import plotly.graph_objects as go
68+
import plotly.graph_objects as go # pyright: ignore[reportMissingTypeStubs]
6869

69-
if not isinstance(x, go.Figure):
70+
if not isinstance(x, go.Figure): # type: ignore
7071
raise TypeError(
7172
f"Don't know how to coerce {x} into a plotly.graph_objects.FigureWidget object."
7273
)

0 commit comments

Comments
 (0)