Skip to content

Commit

Permalink
render_widget now attaches it's return value to the decorated funct…
Browse files Browse the repository at this point in the history
…ion (#119)

Co-authored-by: Barret Schloerke <[email protected]>
  • Loading branch information
cpsievert and schloerke authored Jan 16, 2024
1 parent e0de2d4 commit 4451359
Show file tree
Hide file tree
Showing 15 changed files with 495 additions and 335 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [UNRELEASED]

* 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)
* Reduce default plot margins on plotly graphs.

## [0.2.4] - 2023-11-20
Expand Down
39 changes: 13 additions & 26 deletions examples/altair/app.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
import altair as alt
from shiny import App, render, ui
import shiny.express
from shiny import render
from vega_datasets import data

from shinywidgets import output_widget, reactive_read, register_widget
from shinywidgets import reactive_read, render_altair

source = data.cars()

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

def server(input, output, session):

# Replicate JupyterChart interactivity
# https://altair-viz.github.io/user_guide/jupyter_chart.html#point-selections
# Replicate JupyterChart interactivity
# https://altair-viz.github.io/user_guide/jupyter_chart.html#point-selections
@render_altair
def jchart():
brush = alt.selection_point(name="point", encodings=["color"], bind="legend")
chart = alt.Chart(source).mark_point().encode(
return alt.Chart(data.cars()).mark_point().encode(
x='Horsepower:Q',
y='Miles_per_Gallon:Q',
color=alt.condition(brush, 'Origin:N', alt.value('grey')),
).add_params(brush)

jchart = alt.JupyterChart(chart)

# Display/register the chart in the app_ui
register_widget("chart", jchart)

# Reactive-ly read point selections
@output
@render.text
def selection():
pt = reactive_read(jchart.selections, "point")
return "Selected point: " + str(pt)

app = App(app_ui, server)
76 changes: 33 additions & 43 deletions examples/ipyleaflet/app.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,36 @@
import ipyleaflet as L
from htmltools import css
from shiny import *

from shinywidgets import output_widget, reactive_read, register_widget

app_ui = ui.page_fillable(
ui.div(
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10),
ui.output_text("map_bounds"),
style=css(
display="flex", justify_content="center", align_items="center", gap="2rem"
),
),
output_widget("map"),
)


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)

# When the slider changes, update the map's zoom attribute (2)
@reactive.Effect
def _():
map.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"))

# Everytime the map's bounds change, update the output message (3)
@output
@render.text
def map_bounds():
b = reactive_read(map, "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}"
from shiny import reactive, render, req
from shiny.express import input, ui

from shinywidgets import reactive_read, render_widget

ui.page_opts(title="ipyleaflet demo")

with ui.sidebar():
ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10)

@render_widget
def lmap():
return L.Map(center=(52, 360), zoom=4)

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

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


with ui.card(fill=False):
# Everytime the map's bounds change, update the output message
@render.ui
def map_bounds():
b = reactive_read(lmap.widget, "bounds")
req(b)
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}"
28 changes: 11 additions & 17 deletions examples/ipywidgets/app.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import ipywidgets as ipy
from ipywidgets.widgets.widget import Widget
from shiny import *
import shiny.express
from ipywidgets import IntSlider
from shiny import render

from shinywidgets import *
from shinywidgets import reactive_read, render_widget

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


def server(input: Inputs, output: Outputs, session: Session):
s: Widget = ipy.IntSlider(
@render_widget
def slider():
return IntSlider(
value=7,
min=0,
max=10,
Expand All @@ -21,12 +20,7 @@ def server(input: Inputs, output: Outputs, session: Session):
readout_format="d",
)

register_widget("slider", s)

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


app = App(app_ui, server, debug=True)
@render.ui
def slider_val():
val = reactive_read(slider.widget, "value")
return f"The value of the slider is: {val}"
27 changes: 11 additions & 16 deletions examples/plotly/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import numpy as np
import plotly.graph_objs as go
from shiny import *
from shiny import reactive
from shiny.express import input, ui
from sklearn.linear_model import LinearRegression

from shinywidgets import output_widget, register_widget
from shinywidgets import render_plotly

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

app_ui = ui.page_fillable(
ui.input_checkbox("show_fit", "Show fitted line", value=True),
output_widget("scatterplot"),
)
ui.page_opts(title="Plotly demo", fillable=True)

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

def server(input, output, session):

scatterplot = go.FigureWidget(
@render_plotly
def scatterplot():
return go.FigureWidget(
data=[
go.Scattergl(
x=x,
Expand All @@ -39,11 +38,7 @@ def server(input, output, session):
layout={"showlegend": False},
)

register_widget("scatterplot", scatterplot)

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


app = App(app_ui, server)
@reactive.Effect
def _():
scatterplot.widget.data[1].visible = input.show_fit()
35 changes: 12 additions & 23 deletions examples/pydeck/app.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import pydeck as pdk
from shiny import *
from shiny import reactive
from shiny.express import input, ui

from shinywidgets import *
from shinywidgets import render_pydeck

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

def server(input: Inputs, output: Outputs, session: Session):
ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1)

@render_pydeck
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
"HexagonLayer",
UK_ACCIDENTS_DATA,
get_position=["lng", "lat"],
auto_highlight=True,
Expand All @@ -23,7 +20,6 @@ def server(input: Inputs, output: Outputs, session: Session):
extruded=True,
coverage=1,
)

view_state = pdk.ViewState(
longitude=-1.415,
latitude=52.2323,
Expand All @@ -33,16 +29,9 @@ def server(input: Inputs, output: Outputs, session: Session):
pitch=40.5,
bearing=-27.36,
)
return pdk.Deck(layers=[layer], initial_view_state=view_state)

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

# Register either the deck (or deck_widget) instance
register_widget("pydeck", deck)

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


app = App(app_ui, server)
@reactive.effect()
def _():
deckmap.value.initial_view_state.zoom = input.zoom()
deckmap.value.update()
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ setup_requires =
install_requires =
ipywidgets>=7.6.5
jupyter_core
shiny>=0.5.1.9003
# shiny>=0.6.1.9003
shiny @ git+https://github.com/posit-dev/py-shiny.git
python-dateutil>=2.8.2
# Needed because of https://github.com/python/importlib_metadata/issues/411
importlib-metadata>=4.8.3,<5; python_version < "3.8"
Expand Down
31 changes: 25 additions & 6 deletions shinywidgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,32 @@
__email__ = "[email protected]"
__version__ = "0.2.4.9000"

from ._as_widget import as_widget
from ._dependencies import bokeh_dependency
from ._shinywidgets import (
as_widget,
output_widget,
reactive_read,
register_widget,
from ._output_widget import output_widget
from ._render_widget import (
render_altair,
render_bokeh,
render_plotly,
render_pydeck,
render_widget,
)
from ._shinywidgets import reactive_read, register_widget

__all__ = ("output_widget", "register_widget", "render_widget", "reactive_read", "bokeh_dependency", "as_widget")
__all__ = (
# Render methods first
"render_widget",
"render_altair",
"render_bokeh",
"render_plotly",
"render_pydeck",
# Reactive read second
"reactive_read",
# UI methods third
"output_widget",
# Other methods last
"as_widget",
"bokeh_dependency",
# Soft deprecated
"register_widget",
)
17 changes: 9 additions & 8 deletions shinywidgets/_as_widget.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional

from ipywidgets.widgets.widget import Widget
from ipywidgets.widgets.widget import Widget # pyright: ignore[reportMissingTypeStubs]

from ._dependencies import widget_pkg

Expand Down Expand Up @@ -35,7 +35,7 @@ def as_widget(x: object) -> Widget:

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

def as_widget_bokeh(x: object) -> Optional[Widget]:
try:
from jupyter_bokeh import BokehModel
from jupyter_bokeh import BokehModel # pyright: ignore[reportMissingTypeStubs]
except ImportError:
raise ImportError(
"Install the jupyter_bokeh package to use bokeh with shinywidgets."
Expand All @@ -55,18 +55,19 @@ def as_widget_bokeh(x: object) -> Optional[Widget]:
# TODO: ideally we'd do this in set_layout_defaults() but doing
# `BokehModel(x)._model.sizing_mode = "stretch_both"`
# there, but that doesn't seem to work??
from bokeh.plotting import figure
if isinstance(x, figure):
x.sizing_mode = "stretch_both"
from bokeh.plotting import figure # pyright: ignore[reportMissingTypeStubs]

if isinstance(x, figure): # type: ignore
x.sizing_mode = "stretch_both" # pyright: ignore[reportGeneralTypeIssues]

return BokehModel(x) # type: ignore


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

if not isinstance(x, go.Figure):
if not isinstance(x, go.Figure): # type: ignore
raise TypeError(
f"Don't know how to coerce {x} into a plotly.graph_objects.FigureWidget object."
)
Expand Down
Loading

0 comments on commit 4451359

Please sign in to comment.