Skip to content

Commit c578ea2

Browse files
authored
Export as_widget() and update README to refer to new article (#90)
1 parent 0bbb8b1 commit c578ea2

File tree

4 files changed

+119
-219
lines changed

4 files changed

+119
-219
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
* Closed #94: New `SHINYWIDGETS_CDN` and `SHINYWIDGETS_CDN_ONLY` environment variables were added to more easily specify the CDN provider. Also, the default provider has changed from <unpkg.com> to <cdn.jsdelivr.net/npm> (#95)
1212
* A warning is no longer issued (by default) when the path to a local widget extension is not found. This is because, if an internet connection is available, the widget assests are still loaded via CDN. To restore the previous behavior, set the `SHINYWIDGETS_EXTENSION_WARNING` environment variable to `"true"`. (#95)
1313
* Closed #14: Added a `bokeh_dependency()` function to simplify use of bokeh widgets. (#85)
14+
* Closed #89: Exported the `as_widget()` function to attempt coercion of objects into ipywidgets. Internally, `{shinywidgets}` uses it to implictly coerce objects into ipywidgets, but it can be also useful to explicitly coerce objects before passing to `register_widget()` (so that the ipywidget can then be updated in-place and/or used as a reactive value (`reactive_read()`)). (#90)
1415

1516
## [0.1.6] - 2023-03-24
1617

README.md

Lines changed: 22 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -12,164 +12,41 @@ pip install shinywidgets
1212

1313
## Overview
1414

15-
Every Shiny app has two main parts: the user interface (UI) and server logic.
16-
`{shinywidgets}` provides `output_widget()` for defining where to place a widget in the UI
17-
and `register_widget()` (or `@render_widget`) for supplying a widget-like object to
18-
the `output_widget()` container. More technically, widget-like means:
19-
20-
* Any object that subclasses `{ipywidgets}`'s `Widget` class.
21-
* Some other widget-like object that can be coerced into a `Widget`. Currently, we
22-
support objects from `{altair}`, `{bokeh}`, and `{pydeck}`, but [please let us
23-
know](https://github.com/rstudio/py-shinywidgets/issues/new) about other packages that we
24-
should support.
25-
26-
The recommended way to incorporate `{shinywidgets}` widgets into Shiny apps is to:
27-
28-
1. Initialize and `register_widget()` _once_ for each widget.
29-
* In most cases, initialization should happen when the user session starts (i.e.,
30-
the `server` function first executes), but if the widget is slow to initialize and
31-
doesn't need to be shown right away, you may want to delay that initialization
32-
until it's needed.
33-
2. Use Shiny's `@reactive.Effect` to reactively modify the widget whenever relevant
34-
reactive values change.
35-
3. Use `{shinywidgets}`'s `reactive_read()` to update other outputs whenever the widget changes.
36-
* This way, relevant output(s) invalidate (i.e., recalculate) whenever the relevant
37-
widget attributes change (client-side or server-side).
38-
39-
The following app below uses `{ipyleaflet}` to demonstrate all these concepts:
15+
See the [using ipywidgets section](https://shiny.rstudio.com/py/docs/ipywidgets.html) of the Shiny for Python website.
4016

41-
```py
42-
from shiny import *
43-
from shinywidgets import output_widget, register_widget, reactive_read
44-
import ipyleaflet as L
45-
46-
app_ui = ui.page_fluid(
47-
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10),
48-
output_widget("map"),
49-
ui.output_text("map_bounds"),
50-
)
51-
52-
def server(input, output, session):
17+
## Frequently asked questions
5318

54-
# Initialize and display when the session starts (1)
55-
map = L.Map(center=(52, 360), zoom=4)
56-
register_widget("map", map)
19+
### What ipywidgets are supported?
5720

58-
# When the slider changes, update the map's zoom attribute (2)
59-
@reactive.Effect
60-
def _():
61-
map.zoom = input.zoom()
21+
In theory, shinywidgets supports any instance that inherits from `{ipywidgets}`' `Widget` class.
6222

63-
# When zooming directly on the map, update the slider's value (2 and 3)
64-
@reactive.Effect
65-
def _():
66-
ui.update_slider("zoom", value=reactive_read(map, "zoom"))
23+
That said, `{shinywidgets}` can also "directly" render objects that don't inherit from `Widget`, but have a known way of coercing into a `Widget` instance. This list currently includes:
6724

68-
# Everytime the map's bounds change, update the output message (3)
69-
@output
70-
@render.text
71-
def map_bounds():
72-
b = reactive_read(map, "bounds")
73-
lat = [b[0][0], b[0][1]]
74-
lon = [b[1][0], b[1][1]]
75-
return f"The current latitude is {lat} and longitude is {lon}"
25+
* Altair charts (via the [vega](https://pypi.org/project/vega/) package).
26+
* Bokeh widgets (via the [jupyter_bokeh](https://github.com/bokeh/jupyter_bokeh) package).
27+
* Plotly's `Figure` class (via `FigureWidget`).
28+
* Pydeck's `Deck` class (via it's `.show()` method).
7629

77-
app = App(app_ui, server)
78-
```
30+
[See here](https://github.com/rstudio/py-shinywidgets/blob/main/shinywidgets/_as_widget.py) for more details on how these objects are coerced into `Widget` instances, and if you know of other packages that should be added to this list, please [let us know](https://github.com/rstudio/py-shinywidgets/issues/new)
7931

80-
<div align="center">
81-
<img src="https://user-images.githubusercontent.com/1365941/171508416-1ebe157c-b305-4517-9c89-14891dff8f79.gif" width="70%">
82-
</div>
32+
### Bokeh widgets aren't displaying, what gives?
8333

84-
The style of programming above (display and mutate) is great for efficiently performing
85-
partial updates to a widget. This is really useful when a widget needs to display lots
86-
of data and also quickly handle partial updates; for example, toggling the visibility of
87-
a fitted line on a scatterplot with lots of points:
34+
Similar to how you have to run `bokeh.io.output_notebook()` to run `{bokeh}` in notebook, you also have to explicitly bring the JS/CSS dependencies to the Shiny app, which can be done this way:
8835

8936
```py
90-
from shiny import *
91-
from shinywidgets import output_widget, register_widget
92-
import plotly.graph_objs as go
93-
import numpy as np
94-
from sklearn.linear_model import LinearRegression
95-
96-
# Generate some data and fit a linear regression
97-
n = 10000
98-
d = np.random.RandomState(0).multivariate_normal([0, 0], [(1, 0.5), (0.5, 1)], n).T
99-
fit = LinearRegression().fit(d[0].reshape(-1, 1), d[1])
100-
xgrid = np.linspace(start=min(d[0]), stop=max(d[0]), num=30)
37+
from shiny import ui
38+
from shinywidgets import bokeh_dependencies
10139

10240
app_ui = ui.page_fluid(
103-
output_widget("scatterplot"),
104-
ui.input_checkbox("show_fit", "Show fitted line", value=True),
41+
bokeh_dependencies(),
42+
# ...
10543
)
106-
107-
def server(input, output, session):
108-
109-
scatterplot = go.FigureWidget(
110-
data=[
111-
go.Scattergl(
112-
x=d[0],
113-
y=d[1],
114-
mode="markers",
115-
marker=dict(color="rgba(0, 0, 0, 0.05)", size=5),
116-
),
117-
go.Scattergl(
118-
x=xgrid,
119-
y=fit.intercept_ + fit.coef_[0] * xgrid,
120-
mode="lines",
121-
line=dict(color="red", width=2),
122-
),
123-
]
124-
)
125-
126-
register_widget("scatterplot", scatterplot)
127-
128-
@reactive.Effect
129-
def _():
130-
scatterplot.data[1].visible = input.show_fit()
131-
132-
app = App(app_ui, server)
13344
```
13445

135-
<div align="center">
136-
<img src="https://user-images.githubusercontent.com/1365941/171507230-4b32ce4a-6e80-43a4-9c71-6a1f3ffe443e.gif" width="70%">
137-
</div>
138-
139-
140-
That being said, in a situation where:
141-
142-
* Performant updates aren't important
143-
* Other outputs don't depend on the widget's state
144-
* It's convenient to initialize a widget in a reactive context
145-
146-
Then it's ok to reach for `@render_widget()` (instead of `register_widget()`) which
147-
creates a reactive context (similar to Shiny's `@render_plot()`, `@render_text()`, etc.)
148-
where each time that context gets invalidated, the output gets redrawn from scratch. In
149-
a simple case like the one below, that redrawing may not be noticable, but if you we're
150-
to redraw the entire scatterplot above everytime the fitted line was toggled, there'd
151-
be noticeable delay.
15246

153-
```py
154-
from shiny import *
155-
from shinywidgets import output_widget, render_widget
156-
import ipyleaflet as L
157-
158-
app_ui = ui.page_fluid(
159-
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10),
160-
output_widget("map")
161-
)
162-
163-
def server(input, output, session):
164-
@output
165-
@render_widget
166-
def map():
167-
return L.Map(center=(52, 360), zoom=input.zoom())
168-
169-
app = App(app_ui, server)
170-
```
47+
### Does `{shinywidgets}` work with Shinylive?
17148

172-
## Frequently asked questions
49+
To some extent, yes. As shown on the official [shinylive examples](https://shinylive.io/py/examples/), packages like plotly and ipyleaflet work (as long as you've provided the proper dependencies in a [`requirements.txt` file](https://shinylive.io/py/examples/#extra-packages)), but other packages like altair and qgrid may not work (at least currently) due to missing wheel files and/or dependencies with compiled code that can't be compiled to WebAssembly.
17350

17451
### How do I size the widget?
17552

@@ -179,8 +56,8 @@ including `height` and `width`. So, given a `Widget` instance `w`, you should be
17956
do something like:
18057

18158
```py
182-
w.layout.height = "600px"
183-
w.layout.width = "80%"
59+
w.layout.height = "100%"
60+
w.layout.width = "100%"
18461
```
18562

18663
### How do I hide/show a widget?
@@ -204,18 +81,6 @@ w.layout.display = "none"
20481
Yes! In fact this a crucial aspect to how packages like `{ipyleaflet}` work. In
20582
`{ipyleaflet}`'s case, each [individual marker is a widget](https://ipyleaflet.readthedocs.io/en/latest/layers/circle_marker.html) which gets attached to a `Map()` via `.add_layer()`.
20683

207-
### Does `{shinywidgets}` work with Shinylive?
208-
209-
Shinylive allows some Shiny apps to be statically served (i.e., run entirely in the
210-
browser). [py-shinylive](https://github.com/rstudio/py-shinylive) does have some special
211-
support for `{shinywidgets}` and it's dependencies, which should make most widgets work
212-
out-of-the-box.
213-
214-
In some cases, the package(s) that you want to use may not come pre-bundled with
215-
`{shinywidgets}`; and in that case, you can [include a `requirements.txt`
216-
file](https://shinylive.io/py/examples/#extra-packages) to pre-install those other
217-
packages
218-
21984
## Troubleshooting
22085

22186
If after [installing](#installation) `{shinywidgets}`, you have trouble rendering widgets,
@@ -243,7 +108,8 @@ app_ui = ui.page_fluid(
243108
# ...
244109
)
245110
```
246-
```
111+
112+
247113
#### Other widgets?
248114

249115
Know of another widget that requires initialization code? [Please let us know about

shinywidgets/_as_widget.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from typing import Optional
2+
3+
from ipywidgets.widgets.widget import Widget
4+
5+
from ._dependencies import widget_pkg
6+
7+
__all__ = ("as_widget",)
8+
9+
10+
# Some objects aren't directly renderable as an ipywidget, but in some cases,
11+
# we can coerce them into one
12+
def as_widget(x: object) -> Widget:
13+
if isinstance(x, Widget):
14+
return x
15+
16+
pkg = widget_pkg(x)
17+
18+
_as_widget = AS_WIDGET_MAP.get(pkg, None)
19+
if _as_widget is None:
20+
raise TypeError(f"Don't know how to coerce {x} into a ipywidget.Widget object.")
21+
22+
res = _as_widget(x)
23+
24+
if not isinstance(res, Widget):
25+
raise TypeError(
26+
f"Failed to coerce {x} (an object from package {pkg}) into a ipywidget.Widget object."
27+
)
28+
29+
return res
30+
31+
32+
def as_widget_altair(x: object) -> Optional[Widget]:
33+
try:
34+
from vega.widget import VegaWidget
35+
except ImportError:
36+
raise ImportError("Install the vega package to use altair with shinywidgets.")
37+
38+
if not hasattr(x, "to_dict"):
39+
raise TypeError(
40+
f"Don't know how to coerce {x} (an altair object) into an ipywidget without a .to_dict() method."
41+
)
42+
43+
try:
44+
return VegaWidget(x.to_dict()) # type: ignore
45+
except Exception as e:
46+
raise RuntimeError(f"Failed to coerce {x} into a VegaWidget: {e}")
47+
48+
49+
def as_widget_bokeh(x: object) -> Optional[Widget]:
50+
try:
51+
from jupyter_bokeh import BokehModel
52+
except ImportError:
53+
raise ImportError(
54+
"Install the jupyter_bokeh package to use bokeh with shinywidgets."
55+
)
56+
57+
return BokehModel(x) # type: ignore
58+
59+
60+
def as_widget_plotly(x: object) -> Optional[Widget]:
61+
# Don't need a try import here since this won't be called unless x is a plotly object
62+
import plotly.graph_objects as go
63+
64+
if not isinstance(x, go.Figure):
65+
raise TypeError(
66+
f"Don't know how to coerce {x} into a plotly.graph_objects.FigureWidget object."
67+
)
68+
69+
return go.FigureWidget(x.data, x.layout) # type: ignore
70+
71+
72+
def as_widget_pydeck(x: object) -> Optional[Widget]:
73+
if not hasattr(x, "show"):
74+
raise TypeError(
75+
f"Don't know how to coerce {x} (a pydeck object) into an ipywidget without a .show() method."
76+
)
77+
78+
return x.show() # type: ignore
79+
80+
81+
AS_WIDGET_MAP = {
82+
"altair": as_widget_altair,
83+
"bokeh": as_widget_bokeh,
84+
"plotly": as_widget_plotly,
85+
"pydeck": as_widget_pydeck,
86+
}

0 commit comments

Comments
 (0)