|
1 | 1 | ipyshiny
|
2 | 2 | ================
|
3 | 3 |
|
4 |
| -Render [ipywidgets](https://github.com/jupyter-widgets/ipywidgets) inside a [PyShiny](https://github.com/rstudio/prism) app |
| 4 | +Render [ipywidgets](https://ipywidgets.readthedocs.io/en/stable/) inside a |
| 5 | +[Shiny](https://pyshiny.netlify.app/) app. |
5 | 6 |
|
6 | 7 | ## Installation
|
7 | 8 |
|
8 |
| -First, [install Shiny](https://rstudio.github.io/pyshiny-site/install.html), then: |
9 |
| - |
10 | 9 | ```sh
|
11 |
| -git clone https://github.com/rstudio/ipyshiny |
12 |
| -pip install -e ipyshiny |
| 10 | +pip install ipyshiny --extra-index-url=https://pyshiny.netlify.app/pypi |
13 | 11 | ```
|
14 | 12 |
|
15 |
| -## Usage |
| 13 | +## Overview |
16 | 14 |
|
17 |
| -Coming soon. For now, see/run the `examples/`: |
| 15 | +Every Shiny app has two main parts: the user interface (UI) and server logic. |
| 16 | +`{ipyshiny}` 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: |
18 | 19 |
|
19 |
| -```sh |
20 |
| -shiny run examples/outputs/appy.py |
| 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/ipyshiny/issues/new) about other packages that we |
| 24 | + should support. |
| 25 | + |
| 26 | +The recommended way to incorporate `{ipyshiny}` 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 `{ipyshiny}`'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: |
| 40 | + |
| 41 | +```py |
| 42 | +from shiny import * |
| 43 | +from ipyshiny 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): |
| 53 | + |
| 54 | + # Initialize and display when the session starts (1) |
| 55 | + map = L.Map(center=(52, 360), zoom=4) |
| 56 | + register_widget("map", map) |
| 57 | + |
| 58 | + # When the slider changes, update the map's zoom attribute (2) |
| 59 | + @reactive.Effect() |
| 60 | + def _(): |
| 61 | + map.zoom = input.zoom() |
| 62 | + |
| 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")) |
| 67 | + |
| 68 | + # Everytime the map's bounds change, update the output message (3) |
| 69 | + @output(name="map_bounds") |
| 70 | + @render_text() |
| 71 | + def _(): |
| 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}" |
| 76 | + |
| 77 | +app = App(app_ui, server) |
21 | 78 | ```
|
22 | 79 |
|
23 |
| -## Development |
| 80 | + |
24 | 81 |
|
25 |
| -If you want to do development, run: |
26 | 82 |
|
27 |
| -```sh |
28 |
| -pip install -e . |
29 |
| -cd js && yarn watch |
| 83 | +The style of programming above (display and mutate) is great for efficiently performing |
| 84 | +partial updates to a widget. This is really useful when a widget needs to display lots |
| 85 | +of data and also quickly handle partial updates; for example, toggling the visibility of |
| 86 | +a fitted line on a scatterplot with lots of points: |
| 87 | + |
| 88 | +```py |
| 89 | +from shiny import * |
| 90 | +from ipyshiny import output_widget, register_widget |
| 91 | +import plotly.graph_objs as go |
| 92 | +import numpy as np |
| 93 | +from sklearn.linear_model import LinearRegression |
| 94 | + |
| 95 | +# Generate some data and fit a linear regression |
| 96 | +n = 10000 |
| 97 | +d = np.random.RandomState(0).multivariate_normal([0, 0], [(1, 0.5), (0.5, 1)], n).T |
| 98 | +fit = LinearRegression().fit(d[0].reshape(-1, 1), d[1]) |
| 99 | +xgrid = np.linspace(start=min(d[0]), stop=max(d[0]), num=30) |
| 100 | + |
| 101 | +app_ui = ui.page_fluid( |
| 102 | + output_widget("scatterplot"), |
| 103 | + ui.input_checkbox("show_fit", "Show fitted line", value=True), |
| 104 | +) |
| 105 | + |
| 106 | +def server(input, output, session): |
| 107 | + |
| 108 | + scatterplot = go.FigureWidget( |
| 109 | + data=[ |
| 110 | + go.Scattergl( |
| 111 | + x=d[0], |
| 112 | + y=d[1], |
| 113 | + mode="markers", |
| 114 | + marker=dict(color="rgba(0, 0, 0, 0.05)", size=5), |
| 115 | + ), |
| 116 | + go.Scattergl( |
| 117 | + x=xgrid, |
| 118 | + y=fit.intercept_ + fit.coef_[0] * xgrid, |
| 119 | + mode="lines", |
| 120 | + line=dict(color="red", width=2), |
| 121 | + ), |
| 122 | + ] |
| 123 | + ) |
| 124 | + |
| 125 | + register_widget("scatterplot", scatterplot) |
| 126 | + |
| 127 | + @reactive.Effect() |
| 128 | + def _(): |
| 129 | + scatterplot.data[1].visible = input.show_fit() |
| 130 | + |
| 131 | +app = App(app_ui, server) |
30 | 132 | ```
|
31 | 133 |
|
32 |
| -Additionally, you can install pre-commit hooks which will automatically reformat and lint the code when you make a commit: |
| 134 | + |
33 | 135 |
|
34 |
| -```sh |
35 |
| -pre-commit install |
| 136 | +That being said, in a situation where: |
| 137 | + |
| 138 | +* Performant updates aren't important |
| 139 | +* Other outputs don't depend on the widget's state |
| 140 | +* It's convenient to initialize a widget in a reactive context |
| 141 | + |
| 142 | +Then it's ok to reach for `@render_widget()` (instead of `register_widget()`) which |
| 143 | +creates a reactive context (similar to Shiny's `@render_plot()`, `@render_text()`, etc.) |
| 144 | +where each time that context gets invalidated, the output gets redrawn from scratch. In |
| 145 | +a simple case like the one below, that redrawing may not be noticable, but if you we're |
| 146 | +to redraw the entire scatterplot above everytime the fitted line was toggled, there'd |
| 147 | +be noticeable delay. |
| 148 | + |
| 149 | +```py |
| 150 | +from shiny import * |
| 151 | +from ipyshiny import output_widget, register_widget, reactive_read |
| 152 | +import ipyleaflet as L |
| 153 | + |
| 154 | +app_ui = ui.page_fluid( |
| 155 | + ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10), |
| 156 | + output_widget("map") |
| 157 | +) |
| 158 | + |
| 159 | +def server(input, output, session): |
| 160 | + @output(name="map") |
| 161 | + @render_widget() |
| 162 | + def _(): |
| 163 | + return L.Map(center=(52, 360), zoom=input.zoom()) |
| 164 | + |
| 165 | +app = App(app_ui, server) |
| 166 | +``` |
| 167 | + |
| 168 | +## Frequently asked questions |
36 | 169 |
|
37 |
| -# To disable: |
38 |
| -# pre-commit uninstall |
| 170 | +### How do I size the widget? |
| 171 | + |
| 172 | +`{ipywidgets}`' `Widget` class has [it's own API for setting inline CSS |
| 173 | +styles](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Styling.html), |
| 174 | +including `height` and `width`. So, given a `Widget` instance `w`, you should be able to |
| 175 | +do something like: |
| 176 | + |
| 177 | +```py |
| 178 | +w.layout.height = "600px" |
| 179 | +w.layout.width = "80%" |
| 180 | +``` |
| 181 | + |
| 182 | +### How do I hide/show a widget? |
| 183 | + |
| 184 | +As mentioned above, a `Widget` class should have a `layout` attribute, which can be |
| 185 | +used to set all sorts of CSS styles, including display and visibility. So, if you wanted |
| 186 | +to hide a widget and still have space allocated for it: |
| 187 | + |
| 188 | +```py |
| 189 | +w.layout.visibility = "hidden" |
| 190 | +``` |
| 191 | + |
| 192 | +Or, to not give it any space: |
| 193 | + |
| 194 | +```py |
| 195 | +w.layout.display = "none" |
| 196 | +``` |
| 197 | + |
| 198 | +### Can I render widgets that contain other widgets? |
| 199 | + |
| 200 | +Yes! In fact this a crucial aspect to how packages like `{ipyleaflet}` work. In |
| 201 | +`{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()`. |
| 202 | + |
| 203 | +### Does `{ipyshiny}` work with `shiny static`? |
| 204 | + |
| 205 | +Shiny's `shiny static` CLI command allows some Shiny apps to be statically served (i.e., |
| 206 | +run entirely in the browser). [py-shinylive](https://github.com/rstudio/py-shinylive) |
| 207 | +(the Python package behind `shiny static`) does have some special support for |
| 208 | +`{ipyshiny}` and it's dependencies, which should make most widgets work out-of-the-box. |
| 209 | + |
| 210 | +In some cases, the package(s) that you want to use may not come pre-bundled with |
| 211 | +`{ipyshiny}`; and in that case, you can [include a `requirements.txt` |
| 212 | +file](https://pyshiny.netlify.app/examples/#extra-packages) to pre-install those other |
| 213 | +packages |
| 214 | + |
| 215 | +## Troubleshooting {#troubleshooting} |
| 216 | + |
| 217 | +If after [installing](#installation) `{ipyshiny}`, you have trouble rendering widgets, |
| 218 | +first try running the "hello world" ipywidgets [example](https://github.com/rstudio/ipyshiny/blob/main/examples/ipywidgets/app.py). If that doesn't work, it could be that you have an unsupported version |
| 219 | +of a dependency like `{ipywidgets}` or `{shiny}`. |
| 220 | + |
| 221 | +If you can run the "hello world" example, but "3rd party" widget(s) don't work, first |
| 222 | +check that the extension is properly configured with `jupyter nbextension list`. Even if |
| 223 | +the extension is properly configured, it still may not work right away, especially if |
| 224 | +that widget requires initialization code in a notebook environment. In this case, |
| 225 | +`{ipyshiny}` probably won't work without providing the equivalent setup information to |
| 226 | +Shiny. Some known cases of this are: |
| 227 | + |
| 228 | +#### bokeh |
| 229 | + |
| 230 | +To use `{bokeh}` in notebook, you have to run `bokeh.io.output_notebook()`. The |
| 231 | +equivalent thing in Shiny is to include the following in the UI definition: |
| 232 | + |
| 233 | +```py |
| 234 | +from bokeh.resources import Resources |
| 235 | +head_content(HTML(Resources(mode="inline").render())) |
| 236 | +``` |
| 237 | + |
| 238 | +#### itables |
| 239 | + |
| 240 | +TODO: Provide initialization code |
| 241 | + |
| 242 | +#### Other widgets |
| 243 | + |
| 244 | +Know of another widget that requires initialization code? [Please let us know about |
| 245 | +it](https://github.com/rstudio/ipyshiny/issues/new)! |
| 246 | + |
| 247 | + |
| 248 | +## Development |
| 249 | + |
| 250 | +If you want to do development on `{ipyshiny}`, run: |
| 251 | + |
| 252 | +```sh |
| 253 | +pip install -e . |
| 254 | +cd js && yarn watch |
39 | 255 | ```
|
0 commit comments