Skip to content

Commit 81f2ef5

Browse files
committed
Merge branch 'main' into html-manager-update
2 parents 6335f54 + d4e0f7c commit 81f2ef5

34 files changed

+1506
-1120
lines changed

.github/workflows/pytest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
strategy:
1515
matrix:
16-
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
16+
python-version: [3.8, 3.9, "3.10", "3.11"]
1717
fail-fast: false
1818

1919
steps:
@@ -80,7 +80,7 @@ jobs:
8080

8181
## prod deploy ----
8282
- name: "Deploy to PyPI"
83-
uses: pypa/gh-action-pypi-publish@master
83+
uses: pypa/gh-action-pypi-publish@release/v1
8484
if: startsWith(github.event.release.name, 'shinywidgets')
8585
with:
8686
user: __token__

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,6 @@ ENV/
108108

109109
js/node_modules
110110
typings/
111-
sandbox/
112111
Untitled*.ipynb
112+
113+
rsconnect-python/

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"editor.formatOnSave": true,
1111
"editor.tabSize": 4,
1212
"editor.codeActionsOnSave": {
13-
"source.organizeImports": true
13+
"source.organizeImports": "explicit"
1414
},
1515
},
1616
"isort.args":["--profile", "black"],

CHANGELOG.md

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

88
## [UNRELEASED]
99

10+
11+
12+
## [0.3.2] - 2024-04-16
13+
14+
* Fixed a bug with multiple altair outputs not working inside a `@shiny.render.ui` decorator. (#140)
15+
* `@render_widget` no longer errors out when giving a `altair.FacetChart` class. (#142)
16+
* `@render_widget` no longer fails to serialize `decimal.Decimal` objects. (#138)
17+
18+
## [0.3.1] - 2024-03-01
19+
20+
* Widgets no longer have a "flash" of incorrect size when first rendered. (#133)
21+
* `@render_widget` now works properly with `Widget`s that aren't `DOMWidget`s (i.e., widgets that aren't meant to be displayed directly). As a result, you can now use `@render_widget` to gain a reference to the widget instance, and then use that reference to update the widget's value. (#133)
22+
23+
## [0.3.0] - 2024-01-25
24+
25+
* 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)
26+
* Added decorators for notable packages that require coercion to the `Widget` class: `@render_altair`, `@render_bokeh`, `@render_plotly`, and `@render_pydeck`. Using these decorators (over `@render_widget`) helps with typing on the `widget` attribute. (#119)
27+
* The `.properties()` method on `altair.Chart` object now works as expected again. (#129)
28+
* Reduce default plot margins on plotly graphs.
29+
30+
## [0.2.4] - 2023-11-20
31+
32+
* Fixed several issues with filling layout behavior introduced in 0.2.3. (#124, #125)
33+
* `reactive_read()` now throws a more informative error when attempting to read non-existing or non-trait attributes. (#120)
34+
35+
## [0.2.3] - 2023-11-13
36+
37+
* Widgets now `fill` inside of a `fillable` container by default. For examples, see the [ipyleaflet](https://github.com/posit-dev/py-shinywidgets/blob/main/examples/ipyleaflet/app.py), [plotly](https://github.com/posit-dev/py-shinywidgets/blob/main/examples/plotly/app.py), or other [output](https://github.com/posit-dev/py-shinywidgets/blob/main/examples/outputs/app.py) examples. If this intelligent filling isn't desirable, either provide a `height` or `fillable=False` on `output_widget()`. (#115)
38+
* `as_widget()` uses the new `altair.JupyterChart()` to coerce `altair.Chart()` into a `ipywidgets.widgets.Widget` instance. (#113)
39+
40+
## [0.2.2] - 2023-10-31
41+
42+
* `@render_widget` now builds on `shiny`'s `render.transformer` infrastructure, and as a result, it works more seamlessly in `shiny.express` mode. (#110)
43+
* Closed #104: Officially support for Python 3.7.
44+
45+
## [0.2.1] - 2023-05-15
46+
47+
* Actually export `as_widget()` (it was mistakenly not exported in 0.2.0).
48+
49+
## [0.2.0] - 2023-04-13
50+
51+
* Closed #43: Fixed an issue where widgets would sometimes not load in a dynamic UI context. (#91, #93)
1052
* Closed #14: Added a `bokeh_dependency()` function to simplify use of bokeh widgets. (#85)
53+
* Closed #89: Exported `as_widget()`, which helps to coerce objects into ipywidgets, and is especially helpful for creating ipywidget objects before passing to `register_widget()` (this way, the ipywidget can then be updated in-place and/or used as a reactive value (`reactive_read()`)). (#90)
54+
* 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)
55+
* 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)
56+
* Closed #86: Fixed an issue with `{ipyleaflet}` sometimes becoming unresponsive due to too many mouse move event messages being sent to the server. (#98)
1157

1258
## [0.1.6] - 2023-03-24
1359

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2022 RStudio, PBC
3+
Copyright (c) 2023 Posit Software, PBC
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 3 additions & 240 deletions
Original file line numberDiff line numberDiff line change
@@ -2,253 +2,16 @@ shinywidgets
22
================
33

44
Render [ipywidgets](https://ipywidgets.readthedocs.io/en/stable/) inside a
5-
[Shiny](https://shiny.rstudio.com/py) app.
5+
[Shiny](https://shiny.rstudio.com/py) (for Python) app.
6+
7+
See the [Jupyter Widgets](https://shiny.rstudio.com/py/docs/ipywidgets.html) article on the Shiny for Python website for more details.
68

79
## Installation
810

911
```sh
1012
pip install shinywidgets
1113
```
1214

13-
## Overview
14-
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:
40-
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):
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
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}"
76-
77-
app = App(app_ui, server)
78-
```
79-
80-
<div align="center">
81-
<img src="https://user-images.githubusercontent.com/1365941/171508416-1ebe157c-b305-4517-9c89-14891dff8f79.gif" width="70%">
82-
</div>
83-
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:
88-
89-
```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)
101-
102-
app_ui = ui.page_fluid(
103-
output_widget("scatterplot"),
104-
ui.input_checkbox("show_fit", "Show fitted line", value=True),
105-
)
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)
133-
```
134-
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.
152-
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-
```
171-
172-
## Frequently asked questions
173-
174-
### How do I size the widget?
175-
176-
`{ipywidgets}`' `Widget` class has [it's own API for setting inline CSS
177-
styles](https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Styling.html),
178-
including `height` and `width`. So, given a `Widget` instance `w`, you should be able to
179-
do something like:
180-
181-
```py
182-
w.layout.height = "600px"
183-
w.layout.width = "80%"
184-
```
185-
186-
### How do I hide/show a widget?
187-
188-
As mentioned above, a `Widget` class should have a `layout` attribute, which can be
189-
used to set all sorts of CSS styles, including display and visibility. So, if you wanted
190-
to hide a widget and still have space allocated for it:
191-
192-
```py
193-
w.layout.visibility = "hidden"
194-
```
195-
196-
Or, to not give it any space:
197-
198-
```py
199-
w.layout.display = "none"
200-
```
201-
202-
### Can I render widgets that contain other widgets?
203-
204-
Yes! In fact this a crucial aspect to how packages like `{ipyleaflet}` work. In
205-
`{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()`.
206-
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-
219-
## Troubleshooting
220-
221-
If after [installing](#installation) `{shinywidgets}`, you have trouble rendering widgets,
222-
first try running the "hello world" ipywidgets [example](https://github.com/rstudio/py-shinywidgets/blob/main/examples/ipywidgets/app.py). If that doesn't work, it could be that you have an unsupported version
223-
of a dependency like `{ipywidgets}` or `{shiny}`.
224-
225-
If you can run the "hello world" example, but "3rd party" widget(s) don't work, first
226-
check that the extension is properly configured with `jupyter nbextension list`. Even if
227-
the extension is properly configured, it still may not work right away, especially if
228-
that widget requires initialization code in a notebook environment. In this case,
229-
`{shinywidgets}` probably won't work without providing the equivalent setup information to
230-
Shiny. Some known cases of this are:
231-
232-
#### bokeh
233-
234-
To use `{bokeh}` in notebook, you have to run `bokeh.io.output_notebook()`. The
235-
equivalent thing in Shiny is to include the following in the UI definition:
236-
237-
```py
238-
from shiny import ui
239-
from shinywidgets import bokeh_dependencies
240-
241-
app_ui = ui.page_fluid(
242-
bokeh_dependencies(),
243-
# ...
244-
)
245-
```
246-
```
247-
#### Other widgets?
248-
249-
Know of another widget that requires initialization code? [Please let us know about
250-
it](https://github.com/rstudio/py-shinywidgets/issues/new)!
251-
25215
## Development
25316

25417
If you want to do development on `{shinywidgets}`, run:

examples/altair/app.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import altair as alt
2+
import shiny.express
3+
from shiny import render
4+
from vega_datasets import data
5+
6+
from shinywidgets import reactive_read, render_altair
7+
8+
9+
# Output selection information (click on legend in the plot)
10+
@render.code
11+
def selection():
12+
pt = reactive_read(jchart.widget.selections, "point")
13+
return "Selected point: " + str(pt)
14+
15+
# Replicate JupyterChart interactivity
16+
# https://altair-viz.github.io/user_guide/jupyter_chart.html#point-selections
17+
@render_altair
18+
def jchart():
19+
brush = alt.selection_point(name="point", encodings=["color"], bind="legend")
20+
return alt.Chart(data.cars()).mark_point().encode(
21+
x='Horsepower:Q',
22+
y='Miles_per_Gallon:Q',
23+
color=alt.condition(brush, 'Origin:N', alt.value('grey')),
24+
).add_params(brush)

0 commit comments

Comments
 (0)