Created
March 28, 2023 13:04
-
-
Save gshotwell/92fda56ba726670f30803f29ea71d431 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import datetime | |
from typing import Dict, List, Optional, Tuple | |
import astropy.units as u | |
import matplotlib.dates as mpldates | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import pandas as pd | |
import pytz | |
import suntime | |
import timezonefinder | |
from astropy.coordinates import AltAz, EarthLocation, SkyCoord | |
from location import location_server, location_ui | |
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui | |
app_ui = ui.page_fixed( | |
ui.tags.h3("Air mass calculator"), | |
ui.div( | |
ui.markdown( | |
"""This Shiny app uses [Astropy](https://www.astropy.org/) to calculate the | |
altitude (degrees above the horizon) and airmass (the amount of atmospheric | |
air along your line of sight to an object) of one or more astronomical | |
objects, over a given evening, at a given geographic location. | |
""" | |
), | |
class_="mb-5", | |
), | |
ui.row( | |
ui.column( | |
8, | |
ui.output_ui("timeinfo"), | |
ui.output_plot("plot", height="800px"), | |
# For debugging | |
# ui.output_table("table"), | |
class_="order-2 order-sm-1", | |
), | |
ui.column( | |
4, | |
ui.panel_well( | |
ui.input_date("date", "Date"), | |
class_="pb-1 mb-3", | |
), | |
ui.panel_well( | |
ui.input_text_area( | |
"objects", "Target object(s)", "M1, NGC35, PLX299", rows=3 | |
), | |
class_="pb-1 mb-3", | |
), | |
ui.panel_well( | |
location_ui("location"), | |
class_="mb-3", | |
), | |
class_="order-1 order-sm-2", | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
loc = location_server("location") | |
time_padding = datetime.timedelta(hours=1.5) | |
@reactive.Calc | |
def obj_names() -> List[str]: | |
"""Returns a split and *slightly* cleaned-up list of object names""" | |
req(input.objects()) | |
return [x.strip() for x in input.objects().split(",") if x.strip() != ""] | |
@reactive.Calc | |
def obj_coords() -> List[SkyCoord]: | |
return [SkyCoord.from_name(name) for name in obj_names()] | |
@reactive.Calc | |
def times_utc() -> Tuple[datetime.datetime, datetime.datetime]: | |
req(input.date()) | |
lat, long = loc() | |
sun = suntime.Sun(lat, long) | |
return ( | |
sun.get_sunset_time(input.date()), | |
sun.get_sunrise_time(input.date() + datetime.timedelta(days=1)), | |
) | |
@reactive.Calc | |
def timezone() -> Optional[str]: | |
lat, long = loc() | |
return timezonefinder.TimezoneFinder().timezone_at(lat=lat, lng=long) | |
@reactive.Calc | |
def times_at_loc(): | |
start, end = times_utc() | |
tz = pytz.timezone(timezone()) | |
return (start.astimezone(tz), end.astimezone(tz)) | |
@reactive.Calc | |
def df() -> Dict[str, pd.DataFrame]: | |
start, end = times_at_loc() | |
times = pd.date_range( | |
start - time_padding, | |
end + time_padding, | |
periods=100, | |
) | |
lat, long = loc() | |
eloc = EarthLocation(lat=lat * u.deg, lon=long * u.deg, height=0) | |
altaz_list = [ | |
obj.transform_to(AltAz(obstime=times, location=eloc)) | |
for obj in obj_coords() | |
] | |
return { | |
obj: pd.DataFrame( | |
{ | |
"obj": obj, | |
"time": times, | |
"alt": altaz.alt, | |
# Filter out discontinuity | |
"secz": np.where(altaz.alt > 0, altaz.secz, np.nan), | |
} | |
) | |
for (altaz, obj) in zip(altaz_list, obj_names()) | |
} | |
@output | |
@render.plot | |
def plot(): | |
fig, [ax1, ax2] = plt.subplots(nrows=2) | |
sunset, sunrise = times_at_loc() | |
def add_boundary(ax, xval): | |
ax.axvline(x=xval, c="#888888", ls="dashed") | |
ax1.set_ylabel("Altitude (deg)") | |
ax1.set_xlabel("Time") | |
ax1.set_ylim(-10, 90) | |
ax1.set_xlim(sunset - time_padding, sunrise + time_padding) | |
ax1.grid() | |
add_boundary(ax1, sunset) | |
add_boundary(ax1, sunrise) | |
for obj_name, data in df().items(): | |
ax1.plot(data["time"], data["alt"], label=obj_name) | |
ax1.xaxis.set_major_locator(mpldates.AutoDateLocator()) | |
ax1.xaxis.set_major_formatter( | |
mpldates.DateFormatter("%H:%M", tz=pytz.timezone(timezone())) | |
) | |
ax1.legend(loc="upper right") | |
ax2.set_ylabel("Air mass") | |
ax2.set_xlabel("Time") | |
ax2.set_ylim(4, 1) | |
ax2.set_xlim(sunset - time_padding, sunrise + time_padding) | |
ax2.grid() | |
add_boundary(ax2, sunset) | |
add_boundary(ax2, sunrise) | |
for data in df().values(): | |
ax2.plot(data["time"], data["secz"]) | |
ax2.xaxis.set_major_locator(mpldates.AutoDateLocator()) | |
ax2.xaxis.set_major_formatter( | |
mpldates.DateFormatter("%H:%M", tz=pytz.timezone(timezone())) | |
) | |
return fig | |
@output | |
@render.table | |
def table() -> pd.DataFrame: | |
return pd.concat(df()) | |
@output | |
@render.ui | |
def timeinfo(): | |
start_utc, end_utc = times_utc() | |
start_at_loc, end_at_loc = times_at_loc() | |
return ui.TagList( | |
f"Sunset: {start_utc.strftime('%H:%M')}, ", | |
f"Sunrise: {end_utc.strftime('%H:%M')} ", | |
"(UTC)", | |
ui.tags.br(), | |
f"Sunset: {start_at_loc.strftime('%H:%M')}, ", | |
f"Sunrise: {end_at_loc.strftime('%H:%M')} ", | |
f"({timezone()})", | |
) | |
# The debug=True causes it to print messages to the console. | |
app = App(app_ui, server, debug=False) | |
from typing import Optional | |
import ipyleaflet as L | |
from shinywidgets import output_widget, reactive_read, register_widget | |
from shiny import Inputs, Outputs, Session, module, reactive, req, ui | |
# ============================================================ | |
# Module: location | |
# ============================================================ | |
@module.ui | |
def location_ui( | |
label: str = "Location", | |
*, | |
lat: Optional[float] = None, | |
long: Optional[float] = None, | |
) -> ui.TagChildArg: | |
return ui.div( | |
ui.input_numeric("lat", "Latitude", value=lat), | |
ui.input_numeric("long", "Longitude", value=long), | |
ui.help_text("Click to select location"), | |
output_widget("map", height="200px"), | |
ui.tags.style( | |
""" | |
.jupyter-widgets.leaflet-widgets { | |
height: 100% !important; | |
} | |
""" | |
), | |
) | |
@module.server | |
def location_server( | |
input: Inputs, output: Outputs, session: Session, *, wrap_long: bool = True | |
): | |
map = L.Map(center=(0, 0), zoom=1, scoll_wheel_zoom=True) | |
with reactive.isolate(): | |
marker = L.Marker(location=(input.lat() or 0, input.long() or 0)) | |
with reactive.isolate(): # Use this to ensure we only execute one time | |
if input.lat() is None and input.long() is None: | |
ui.notification_show( | |
"Searching for location...", duration=99999, id="searching" | |
) | |
ui.insert_ui( | |
ui.tags.script( | |
""" | |
navigator.geolocation.getCurrentPosition( | |
({coords}) => { | |
const {latitude, longitude, altitude} = coords; | |
Shiny.setInputValue("#HERE#", {latitude, longitude}); | |
}, | |
(err) => { | |
Shiny.setInputValue("#HERE#", {latitude: 0, longitude: 0}); | |
}, | |
{maximumAge: Infinity, timeout: Infinity} | |
) | |
""".replace( | |
"#HERE#", module.resolve_id("here") | |
) | |
), | |
selector="body", | |
where="beforeEnd", | |
immediate=True, | |
) | |
@reactive.isolate() | |
def update_text_inputs(lat: Optional[float], long: Optional[float]) -> None: | |
req(lat is not None, long is not None) | |
lat = round(lat, 8) | |
long = round(long, 8) | |
if lat != input.lat(): | |
input.lat.freeze() | |
ui.update_text("lat", value=lat) | |
if long != input.long(): | |
input.long.freeze() | |
ui.update_text("long", value=long) | |
map.center = (lat, long) | |
@reactive.isolate() | |
def update_marker(lat: Optional[float], long: Optional[float]) -> None: | |
req(lat is not None, long is not None) | |
lat = round(lat, 8) | |
long = round(long, 8) | |
if marker.location != (lat, long): | |
marker.location = (lat, long) | |
if marker not in map.layers: | |
map.add_layer(marker) | |
map.center = marker.location | |
def on_map_interaction(**kwargs): | |
if kwargs.get("type") == "click": | |
lat, long = kwargs.get("coordinates") | |
update_text_inputs(lat, long) | |
map.on_interaction(on_map_interaction) | |
register_widget("map", map) | |
@reactive.Effect | |
def _(): | |
coords = reactive_read(marker, "location") | |
if coords: | |
update_text_inputs(coords[0], coords[1]) | |
@reactive.Effect | |
def sync_autolocate(): | |
coords = input.here() | |
ui.notification_remove("searching") | |
if coords and not input.lat() and not input.long(): | |
update_text_inputs(coords["latitude"], coords["longitude"]) | |
@reactive.Effect | |
def sync_inputs_to_marker(): | |
update_marker(input.lat(), input.long()) | |
@reactive.Calc | |
def location(): | |
"""Returns tuple of (lat,long) floats--or throws silent error if no lat/long is | |
selected""" | |
# Require lat/long to be populated before we can proceed | |
req(input.lat() is not None, input.long() is not None) | |
try: | |
long = input.long() | |
# Wrap longitudes so they're within [-180, 180] | |
if wrap_long: | |
long = (long + 180) % 360 - 180 | |
return (input.lat(), long) | |
except ValueError: | |
raise ValueError("Invalid latitude/longitude specification") | |
return location | |
astropy | |
ipyleaflet | |
matplotlib | |
numpy | |
pandas | |
pytz | |
shiny | |
shinywidgets | |
suntime | |
timezonefinder | |
# | |
# This file is autogenerated by pip-compile with python 3.9 | |
# To update, run: | |
# | |
# pip-compile | |
# | |
anyio==3.6.1 | |
# via starlette | |
appdirs==1.4.4 | |
# via shiny | |
appnope==0.1.3 | |
# via | |
# ipykernel | |
# ipython | |
asgiref==3.5.2 | |
# via shiny | |
astropy==5.1 | |
# via -r requirements.in | |
asttokens==2.0.8 | |
# via stack-data | |
backcall==0.2.0 | |
# via ipython | |
branca==0.5.0 | |
# via ipyleaflet | |
cffi==1.15.1 | |
# via timezonefinder | |
click==8.1.3 | |
# via | |
# shiny | |
# uvicorn | |
contextvars==2.4 | |
# via shiny | |
contourpy==1.0.5 | |
# via matplotlib | |
cycler==0.11.0 | |
# via matplotlib | |
debugpy==1.6.3 | |
# via ipykernel | |
decorator==5.1.1 | |
# via ipython | |
entrypoints==0.4 | |
# via jupyter-client | |
executing==1.1.0 | |
# via stack-data | |
fonttools==4.37.3 | |
# via matplotlib | |
h11==0.14.0 | |
# via uvicorn | |
h3==3.7.4 | |
# via timezonefinder | |
htmltools==0.1.2 | |
# via shiny | |
idna==3.4 | |
# via anyio | |
immutables==0.19 | |
# via contextvars | |
ipykernel==6.16.0 | |
# via ipywidgets | |
ipyleaflet==0.17.1 | |
# via -r requirements.in | |
ipython==8.5.0 | |
# via | |
# ipykernel | |
# ipywidgets | |
ipywidgets==8.0.2 | |
# via | |
# ipyleaflet | |
# shinywidgets | |
jedi==0.18.1 | |
# via ipython | |
jinja2==3.1.2 | |
# via branca | |
jupyter-client==7.3.5 | |
# via ipykernel | |
jupyter-core==4.11.2 | |
# via | |
# jupyter-client | |
# shinywidgets | |
jupyterlab-widgets==3.0.3 | |
# via ipywidgets | |
kiwisolver==1.4.4 | |
# via matplotlib | |
linkify-it-py==2.0.0 | |
# via shiny | |
markdown-it-py==2.1.0 | |
# via | |
# mdit-py-plugins | |
# shiny | |
markupsafe==2.1.1 | |
# via jinja2 | |
matplotlib==3.6.0 | |
# via -r requirements.in | |
matplotlib-inline==0.1.6 | |
# via | |
# ipykernel | |
# ipython | |
mdit-py-plugins==0.3.1 | |
# via shiny | |
mdurl==0.1.2 | |
# via markdown-it-py | |
nest-asyncio==1.5.5 | |
# via | |
# ipykernel | |
# jupyter-client | |
numpy==1.23.3 | |
# via | |
# -r requirements.in | |
# astropy | |
# contourpy | |
# matplotlib | |
# pandas | |
# pyerfa | |
# timezonefinder | |
packaging==21.3 | |
# via | |
# astropy | |
# htmltools | |
# ipykernel | |
# matplotlib | |
pandas==1.5.0 | |
# via -r requirements.in | |
parso==0.8.3 | |
# via jedi | |
pexpect==4.8.0 | |
# via ipython | |
pickleshare==0.7.5 | |
# via ipython | |
pillow==9.2.0 | |
# via matplotlib | |
prompt-toolkit==3.0.31 | |
# via ipython | |
psutil==5.9.2 | |
# via ipykernel | |
ptyprocess==0.7.0 | |
# via pexpect | |
pure-eval==0.2.2 | |
# via stack-data | |
pycparser==2.21 | |
# via cffi | |
pyerfa==2.0.0.1 | |
# via astropy | |
pygments==2.13.0 | |
# via ipython | |
pyparsing==3.0.9 | |
# via | |
# matplotlib | |
# packaging | |
python-dateutil==2.8.2 | |
# via | |
# jupyter-client | |
# matplotlib | |
# pandas | |
# shinywidgets | |
# suntime | |
python-multipart==0.0.5 | |
# via shiny | |
pytz==2022.2.1 | |
# via | |
# -r requirements.in | |
# pandas | |
pyyaml==6.0 | |
# via astropy | |
pyzmq==24.0.1 | |
# via | |
# ipykernel | |
# jupyter-client | |
shiny==0.2.6 | |
# via | |
# -r requirements.in | |
# shinywidgets | |
shinywidgets==0.1.2 | |
# via -r requirements.in | |
six==1.16.0 | |
# via | |
# python-dateutil | |
# python-multipart | |
sniffio==1.3.0 | |
# via anyio | |
stack-data==0.5.1 | |
# via ipython | |
starlette==0.21.0 | |
# via shiny | |
suntime==1.2.5 | |
# via -r requirements.in | |
timezonefinder==6.1.3 | |
# via -r requirements.in | |
tornado==6.2 | |
# via | |
# ipykernel | |
# jupyter-client | |
traitlets==5.4.0 | |
# via | |
# ipykernel | |
# ipython | |
# ipywidgets | |
# jupyter-client | |
# jupyter-core | |
# matplotlib-inline | |
# traittypes | |
traittypes==0.2.1 | |
# via ipyleaflet | |
typing-extensions==4.3.0 | |
# via | |
# htmltools | |
# shiny | |
# starlette | |
uc-micro-py==1.0.1 | |
# via linkify-it-py | |
uvicorn==0.18.3 | |
# via shiny | |
wcwidth==0.2.5 | |
# via prompt-toolkit | |
websockets==10.3 | |
# via shiny | |
widgetsnbextension==4.0.3 | |
# via ipywidgets | |
xyzservices==2022.9.0 | |
# via ipyleaflet | |
# The following packages are considered to be unsafe in a requirements file: | |
# setuptools | |
import math | |
from pathlib import Path | |
from brownian_motion import brownian_data, brownian_widget | |
from mediapipe import hand_to_camera_eye, xyz_mean | |
from shinymediapipe import input_hand | |
from shinywidgets import output_widget, register_widget | |
from shiny import App, reactive, render, req, ui | |
# Check that JS prerequisites are installed | |
if not (Path(__file__).parent / "shinymediapipe" / "node_modules").is_dir(): | |
raise RuntimeError( | |
"Mediapipe dependencies are not installed. " | |
"Please run `npm install` in the 'shinymediapipe' subdirectory." | |
) | |
# Set to True to see underlying XYZ values and canvas | |
debug = True | |
app_ui = ui.page_fluid( | |
ui.input_action_button("data_btn", "New Data"), | |
output_widget("plot"), | |
input_hand("hand", debug=debug, throttle_delay_secs=0.05), | |
( | |
ui.panel_fixed( | |
ui.div(ui.tags.strong("x:"), ui.output_text("x_debug", inline=True)), | |
ui.div(ui.tags.strong("y:"), ui.output_text("y_debug", inline=True)), | |
ui.div(ui.tags.strong("z:"), ui.output_text("z_debug", inline=True)), | |
ui.div(ui.tags.strong("mag:"), ui.output_text("mag_debug", inline=True)), | |
left="12px", | |
bottom="12px", | |
width="200px", | |
height="auto", | |
class_="d-flex flex-column justify-content-end", | |
) | |
if debug | |
else None | |
), | |
class_="p-3", | |
) | |
def server(input, output, session): | |
# BROWNIAN MOTION ==== | |
@reactive.Calc | |
def random_walk(): | |
"""Generates brownian data whenever 'New Data' is clicked""" | |
input.data_btn() | |
return brownian_data(n=200) | |
# Create Plotly 3D widget and bind it to output_widget("plot") | |
widget = brownian_widget(600, 600) | |
register_widget("plot", widget) | |
@reactive.Effect | |
def update_plotly_data(): | |
walk = random_walk() | |
layer = widget.data[0] | |
layer.x = walk["x"] | |
layer.y = walk["y"] | |
layer.z = walk["z"] | |
layer.marker.color = walk["z"] | |
# HAND TRACKING ==== | |
@reactive.Calc | |
def camera_eye(): | |
"""The eye position, as reflected by the hand input""" | |
hand_val = input.hand() | |
req(hand_val) | |
res = hand_to_camera_eye(hand_val, detect_ok=True) | |
req(res) | |
return res | |
# The raw data is a little jittery. Smooth it out by averaging a few samples | |
smooth_camera_eye = reactive_smooth(n_samples=5, smoother=xyz_mean)(camera_eye) | |
@reactive.Effect | |
def update_plotly_camera(): | |
"""Update Plotly camera using the hand tracking""" | |
widget.layout.scene.camera.eye = smooth_camera_eye() | |
# DEBUGGING ==== | |
@output | |
@render.text | |
def x_debug(): | |
return camera_eye()["x"] | |
@output | |
@render.text | |
def y_debug(): | |
return camera_eye()["y"] | |
@output | |
@render.text | |
def z_debug(): | |
return camera_eye()["z"] | |
@output | |
@render.text | |
def mag_debug(): | |
eye = camera_eye() | |
return f"{round(math.sqrt(eye['x']**2 + eye['y']**2 + eye['z']**2), 2)}" | |
app = App(app_ui, server) | |
def reactive_smooth(n_samples, smoother, *, filter_none=True): | |
"""Decorator for smoothing out reactive calculations over multiple samples""" | |
def wrapper(calc): | |
buffer = [] # Ring buffer of capacity `n_samples` | |
result = reactive.Value(None) # Holds the most recent smoothed result | |
@reactive.Effect | |
def _(): | |
# Get latest value. Because this is happening in a reactive Effect, we'll | |
# automatically take a reactive dependency on whatever is happening in the | |
# calc(). | |
new_value = calc() | |
buffer.append(new_value) | |
while len(buffer) > n_samples: | |
buffer.pop(0) | |
if not filter_none: | |
result.set(smoother(buffer)) | |
else: | |
# The filter cannot handle None values; remove any in the buffer | |
filt_samples = [s for s in buffer if s is not None] | |
if len(filt_samples) == 0: | |
result.set(None) | |
else: | |
result.set(smoother(filt_samples)) | |
# The return value for the wrapper | |
return result.get | |
return wrapper | |
import numpy as np | |
from plotly import graph_objects as go | |
# Code taken from https://plotly.com/python/3d-line-plots/ | |
# * `brownian_motion()` is a function that generates a random walk | |
# * `brownian_widget()` uses restructured plotting code given in article | |
rs = np.random.RandomState() | |
# https://plotly.com/python/3d-line-plots/ | |
def brownian_motion(T=1, N=100, mu=0.1, sigma=0.01, S0=20): | |
dt = float(T) / N | |
t = np.linspace(0, T, N) | |
W = rs.standard_normal(size=N) | |
W = np.cumsum(W) * np.sqrt(dt) # standard brownian motion | |
X = (mu - 0.5 * sigma**2) * t + sigma * W | |
S = S0 * np.exp(X) # geometric brownian motion | |
return S | |
def brownian_data(n=100, mu=(0.0, 0.00), sigma=(0.1, 0.1), S0=(1.0, 1.0)): | |
return { | |
"x": brownian_motion(T=1, N=n, mu=mu[0], sigma=sigma[0], S0=S0[0]), | |
"y": brownian_motion(T=1, N=n, mu=mu[1], sigma=sigma[1], S0=S0[1]), | |
# "y": [i for i in range(n)], | |
"z": [i for i in range(n)], | |
} | |
def brownian_widget(width=600, height=600): | |
widget = go.FigureWidget( | |
data=[ | |
go.Scatter3d( | |
x=[], | |
y=[], | |
z=[], | |
marker=dict( | |
size=4, | |
color=[], | |
colorscale="Viridis", | |
), | |
line=dict(color="darkblue", width=2), | |
) | |
], | |
layout={"showlegend": False, "width": width, "height": height}, | |
) | |
return widget | |
import math | |
from functools import reduce | |
from operator import add | |
from statistics import mean | |
import numpy as np | |
WRIST = 0 | |
THUMB_CMC = 1 | |
THUMB_MCP = 2 | |
THUMB_IP = 3 | |
THUMB_TIP = 4 | |
INDEX_FINGER_MCP = 5 | |
INDEX_FINGER_PIP = 6 | |
INDEX_FINGER_DIP = 7 | |
INDEX_FINGER_TIP = 8 | |
MIDDLE_FINGER_MCP = 9 | |
MIDDLE_FINGER_PIP = 10 | |
MIDDLE_FINGER_DIP = 11 | |
MIDDLE_FINGER_TIP = 12 | |
RING_FINGER_MCP = 13 | |
RING_FINGER_PIP = 14 | |
RING_FINGER_DIP = 15 | |
RING_FINGER_TIP = 16 | |
PINKY_MCP = 17 | |
PINKY_PIP = 18 | |
PINKY_DIP = 19 | |
PINKY_TIP = 20 | |
def hand_to_camera_eye(hands, detect_ok=False): | |
# TODO: Pointing straight at the camera should not change (much) from the default | |
# position | |
# TODO: Use spherical coordinates instead of cartesian | |
left_hand = hands["multiHandedness"][0]["index"] == 0 | |
hand = hands["multiHandLandmarks"][0] | |
def rel_hand(start_pos: int, end_pos: int): | |
return np.subtract(list(hand[start_pos].values()), list(hand[end_pos].values())) | |
if detect_ok: | |
# If the distance between the thumbtip and index finger tip are pretty close, | |
# ignore the hand. (Using "OK" sign to pause hand tracking) | |
ok_dist = np.linalg.norm(rel_hand(THUMB_TIP, INDEX_FINGER_TIP)) | |
ref_dist = np.linalg.norm(rel_hand(INDEX_FINGER_TIP, INDEX_FINGER_DIP)) | |
if ok_dist < ref_dist * 2: | |
return None | |
if not left_hand: | |
normal_vec = np.cross( | |
rel_hand(PINKY_MCP, WRIST), | |
rel_hand(INDEX_FINGER_MCP, WRIST), | |
) | |
else: | |
normal_vec = np.cross( | |
rel_hand(INDEX_FINGER_MCP, WRIST), | |
rel_hand(PINKY_MCP, WRIST), | |
) | |
normal_unit_vec = normal_vec / np.linalg.norm(normal_vec) | |
def list_to_xyz(x): | |
x = list(map(lambda y: round(y, 2), x)) | |
return dict(x=x[0], y=x[1], z=x[2]) | |
# Invert, for some reason | |
normal_unit_vec = normal_unit_vec * -1.0 | |
normal_unit_vec[1] *= 2.0 | |
# Stay a consistent distance from the origin | |
dist = 2 | |
magnitude = math.sqrt(reduce(add, [a**2 for a in normal_unit_vec])) | |
normal_unit_vec = [a / magnitude * dist for a in normal_unit_vec] | |
new_eye = list_to_xyz(normal_unit_vec) | |
return { | |
# Adjust locations to match camera position | |
"x": new_eye["z"], | |
"y": new_eye["x"], | |
"z": new_eye["y"], | |
} | |
def xyz_mean(points): | |
return dict( | |
x=mean([p["x"] for p in points]), | |
y=mean([p["y"] for p in points]), | |
z=mean([p["z"] for p in points]), | |
) | |
numpy | |
shiny | |
plotly | |
shinywidgets | |
from ._hand import dependencies, input_hand, hand_options | |
__all__ = ( | |
"dependencies", | |
"hand_options", | |
"input_hand", | |
) | |
import json | |
from typing import Any, Optional | |
from htmltools import HTMLDependency, Tag, tags | |
from shiny.module import resolve_id | |
HandOptions = dict[str, Any] | |
def dependencies() -> list[HTMLDependency]: | |
def subdep(name: str) -> HTMLDependency: | |
return HTMLDependency( | |
f"@mediapipe/{name}", | |
"1.0.0", | |
source={ | |
"package": "shinymediapipe", | |
"subdir": f"node_modules/@mediapipe/{name}", | |
}, | |
script=[{"src": f"{name}.js"}], | |
) | |
return [ | |
subdep("camera_utils"), | |
subdep("control_utils"), | |
subdep("drawing_utils"), | |
subdep("hands"), | |
HTMLDependency( | |
"shinymediapipe-hands", | |
"1.0.0", | |
source={"package": "shinymediapipe", "subdir": ""}, | |
script={"src": "index.js"}, | |
), | |
] | |
def input_hand( | |
id: str, | |
options: Optional[HandOptions] = None, | |
*, | |
debug: bool = False, | |
throttle_delay_secs: float = 0.1, | |
precision: int = 3, | |
) -> Tag: | |
id = resolve_id(id) | |
if options is None: | |
options = hand_options() | |
return tags.template( | |
{ | |
"id": id, | |
"class": "mediapipe-hand-input", | |
"data-throttle-delay": throttle_delay_secs * 1000, | |
"data-precision": precision, | |
}, | |
{"class": "mediapipe-hand-input-debug"} if debug else None, | |
dependencies(), | |
tags.script(json.dumps(options), type="application/json"), | |
) | |
# https://google.github.io/mediapipe/solutions/hands.html#configuration-options | |
def hand_options( | |
*, | |
maxNumHands: int = 1, | |
modelComplexity: float = 1.0, | |
minDetectionConfidence: float = 0.9, | |
minTrackingConfidence: float = 0.9, | |
**kwargs, | |
) -> HandOptions: | |
maxNumHands = int(maxNumHands) | |
modelComplexity = float(modelComplexity) | |
minDetectionConfidence = float(minDetectionConfidence) | |
minTrackingConfidence = float(minTrackingConfidence) | |
assert 0 < maxNumHands | |
assert 0 <= modelComplexity <= 1 | |
assert 0 <= minDetectionConfidence <= 1 | |
assert 0 <= minTrackingConfidence <= 1 | |
return { | |
"staticImageMode": False, | |
"maxNumHands": maxNumHands, | |
"modelComplexity": modelComplexity, | |
"minDetectionConfidence": minDetectionConfidence, | |
"minTrackingConfidence": minTrackingConfidence, | |
# Future model expansion | |
**kwargs, | |
} | |
function mediapipeHand({callback, videoElement, canvasElement, options = {}, debug = false} = {}) { | |
// Code taken from https://google.github.io/mediapipe/solutions/hands.html | |
// and modified to set the Shiny input value when a hand is detected. | |
const canvasCtx = canvasElement.getContext('2d'); | |
function onResults(results) { | |
// Set Shiny input value | |
if (results.multiHandLandmarks.length > 0) { | |
callback(results); | |
} else { | |
callback(null); | |
} | |
// Do not draw anything unless debugging | |
if (!debug) return; | |
// Draw hand on canvas | |
canvasCtx.save(); | |
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); | |
canvasCtx.drawImage( | |
results.image, 0, 0, canvasElement.width, canvasElement.height); | |
if (results.multiHandLandmarks) { | |
for (const landmarks of results.multiHandLandmarks) { | |
drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, | |
{color: '#00FF00', lineWidth: 2}); | |
drawLandmarks(canvasCtx, landmarks, {color: '#FFFFFF', radius: 1}); | |
} | |
} | |
canvasCtx.restore(); | |
} | |
const hands = new Hands({locateFile: (file) => { | |
// TODO: This is totally cheating; this path is supposed to be an | |
// implementation detail | |
return `lib/@mediapipe/hands-1.0.0/${file}`; | |
}}); | |
hands.setOptions(options); | |
hands.onResults(onResults); | |
const camera = new Camera(videoElement, { | |
onFrame: async () => { | |
await hands.send({image: videoElement}); | |
}, | |
width: 640, | |
height: 480 | |
}); | |
camera.start(); | |
} | |
class HandInputBinding extends Shiny.InputBinding { | |
find(scope) { | |
return $(scope).find("template.mediapipe-hand-input"); | |
} | |
initialize(el) { | |
} | |
getValue(el) { | |
return el.mediapipe_result || null; | |
} | |
setValue(el, value) { | |
console.warn("setValue called on HandInputBinding, this is not implemented"); | |
} | |
getRatePolicy(el) { | |
return { | |
policy: "throttle", | |
delay: el.dataset.throttleDelay ?? 100 | |
}; | |
} | |
subscribe(el, callback) { | |
const id = this.getId(el); | |
const options = JSON.parse(el.content.querySelector("script").innerText); | |
const debug = el.matches(".mediapipe-hand-input-debug"); | |
const precision = el.dataset.precision || 3; | |
const videoEl = document.createElement("video"); | |
videoEl.style.display = "none"; | |
const canvasEl = document.createElement("canvas"); | |
canvasEl.style.display = debug ? "block" : "none"; | |
canvasEl.style.position = "fixed"; | |
canvasEl.style.right = "12px"; | |
canvasEl.style.bottom = "12px"; | |
canvasEl.style.width = "320px"; | |
canvasEl.style.height = "240px"; | |
canvasEl.style.opacity = 0.85; | |
document.body.appendChild(videoEl); | |
document.body.appendChild(canvasEl); | |
mediapipeHand({callback: (value) => { | |
// We should allow the options to indicate what values are desired by the server. | |
if (value) { | |
// Quick and dirty rounding to 4 decimals | |
value = JSON.parse(JSON.stringify(value, function(key, value) { | |
if (key === "image") { | |
return undefined; | |
} | |
if (typeof(value) === "number") { | |
return +value.toFixed(precision); | |
} else { | |
return value; | |
} | |
})); | |
} | |
el.mediapipe_result = value; | |
callback(true); | |
}, videoElement: videoEl, canvasElement: canvasEl, options, debug}); | |
} | |
unsubscribe(el) { | |
// TODO: Destroy everything | |
} | |
} | |
// var handInputBinding = new Shiny.InputBinding(); | |
// $.extend(handInputBinding, { | |
// find: function(scope) { | |
// return $(scope).find(".increment"); | |
// }, | |
// getValue: function(el) { | |
// return parseInt($(el).text()); | |
// }, | |
// setValue: function(el, value) { | |
// $(el).text(value); | |
// }, | |
// subscribe: function(el, callback) { | |
// $(el).on("change.incrementBinding", function(e) { | |
// callback(); | |
// }); | |
// }, | |
// unsubscribe: function(el) { | |
// $(el).off(".incrementBinding"); | |
// } | |
// }); | |
Shiny.inputBindings.register(new HandInputBinding()); | |
{ | |
"name": "shinymediapipe", | |
"version": "1.0.0", | |
"lockfileVersion": 2, | |
"requires": true, | |
"packages": { | |
"": { | |
"name": "shinymediapipe", | |
"version": "1.0.0", | |
"license": "Apache-2.0", | |
"dependencies": { | |
"@mediapipe/camera_utils": "^0.3.1640029074", | |
"@mediapipe/control_utils": "^0.6.1629159505", | |
"@mediapipe/drawing_utils": "^0.3.1620248257", | |
"@mediapipe/hands": "^0.4.1646424915" | |
} | |
}, | |
"node_modules/@mediapipe/camera_utils": { | |
"version": "0.3.1640029074", | |
"resolved": "https://registry.npmjs.org/@mediapipe/camera_utils/-/camera_utils-0.3.1640029074.tgz", | |
"integrity": "sha512-jRV/Mp2lgqNYT68TeVRu/Xq+ptZO9F9vCjxzQsA3VM2r7oXg0TrnfO9De2KKOUTpt0p28dKHs625J8GdWjha8A==" | |
}, | |
"node_modules/@mediapipe/control_utils": { | |
"version": "0.6.1629159505", | |
"resolved": "https://registry.npmjs.org/@mediapipe/control_utils/-/control_utils-0.6.1629159505.tgz", | |
"integrity": "sha512-n8ZfgwZlv3EDiF2bsz81+74qWqbu5JSukxRRVuJqkqxO1CsfQGFj8XI2Cxj0UlsnO1vzNelZiWV1tP3of47z7g==" | |
}, | |
"node_modules/@mediapipe/drawing_utils": { | |
"version": "0.3.1620248257", | |
"resolved": "https://registry.npmjs.org/@mediapipe/drawing_utils/-/drawing_utils-0.3.1620248257.tgz", | |
"integrity": "sha512-s598oo1K6C62mX3rWXJ7n1RJFdXjyQn3f6CeI+lU6kD69MVyBcV3hdmO5LnEZlCw5NRDANtaM8WAOuqZHaldLg==" | |
}, | |
"node_modules/@mediapipe/hands": { | |
"version": "0.4.1646424915", | |
"resolved": "https://registry.npmjs.org/@mediapipe/hands/-/hands-0.4.1646424915.tgz", | |
"integrity": "sha512-R1VM3DRCKTA49nVvkprInYUXx8cKisi86y6/9clvYA0vApmLqTjIHQFibJDHwSdy4Rykn2CjWywQAWw5+mGw8w==" | |
} | |
}, | |
"dependencies": { | |
"@mediapipe/camera_utils": { | |
"version": "0.3.1640029074", | |
"resolved": "https://registry.npmjs.org/@mediapipe/camera_utils/-/camera_utils-0.3.1640029074.tgz", | |
"integrity": "sha512-jRV/Mp2lgqNYT68TeVRu/Xq+ptZO9F9vCjxzQsA3VM2r7oXg0TrnfO9De2KKOUTpt0p28dKHs625J8GdWjha8A==" | |
}, | |
"@mediapipe/control_utils": { | |
"version": "0.6.1629159505", | |
"resolved": "https://registry.npmjs.org/@mediapipe/control_utils/-/control_utils-0.6.1629159505.tgz", | |
"integrity": "sha512-n8ZfgwZlv3EDiF2bsz81+74qWqbu5JSukxRRVuJqkqxO1CsfQGFj8XI2Cxj0UlsnO1vzNelZiWV1tP3of47z7g==" | |
}, | |
"@mediapipe/drawing_utils": { | |
"version": "0.3.1620248257", | |
"resolved": "https://registry.npmjs.org/@mediapipe/drawing_utils/-/drawing_utils-0.3.1620248257.tgz", | |
"integrity": "sha512-s598oo1K6C62mX3rWXJ7n1RJFdXjyQn3f6CeI+lU6kD69MVyBcV3hdmO5LnEZlCw5NRDANtaM8WAOuqZHaldLg==" | |
}, | |
"@mediapipe/hands": { | |
"version": "0.4.1646424915", | |
"resolved": "https://registry.npmjs.org/@mediapipe/hands/-/hands-0.4.1646424915.tgz", | |
"integrity": "sha512-R1VM3DRCKTA49nVvkprInYUXx8cKisi86y6/9clvYA0vApmLqTjIHQFibJDHwSdy4Rykn2CjWywQAWw5+mGw8w==" | |
} | |
} | |
} | |
{ | |
"name": "shinymediapipe", | |
"version": "1.0.0", | |
"description": "", | |
"main": "index.js", | |
"scripts": { | |
"test": "echo \"Error: no test specified\" && exit 1" | |
}, | |
"author": "", | |
"license": "Apache-2.0", | |
"dependencies": { | |
"@mediapipe/camera_utils": "^0.3.1640029074", | |
"@mediapipe/control_utils": "^0.6.1629159505", | |
"@mediapipe/drawing_utils": "^0.3.1620248257", | |
"@mediapipe/hands": "^0.4.1646424915" | |
} | |
} | |
ß | |
ß | |
† | |
† | |
† | |
† | |
† | |
† | |
¶ | |
† | |
† | |
¶ | |
† | |
† | |
† | |
† | |
† | |
† | |
† | |
annotation⁄id)⁄copyr | |
selected_datas | |
Nc | |
<listcomp>z+server.<locals>.ts_data.<locals>.<listcomp>0 | |
⁄pd⁄ DataFrame⁄np⁄array⁄range⁄get⁄merge⁄fillna⁄print⁄type)⁄ dataframe⁄ annotatedr | |
| |
Ò | |
Ù | |
à | |
çdê9âoåo—‘–ÿ–r" | |
r" | |
Ò | |
Ù | |
à | |
r" | |
Nr1 | |
rF | |
input_text⁄input_action_button)r | |
ö | |
†l∞B—7‘7ÒÙ | |
Ù | |
ÒÙ | |
⁄reactive⁄Value⁄Effect⁄eventrS | |
r | |
Ñ_› | |
á^Ç^êE‘)—*‘*( | |
brush_opts⁄ output_ui⁄output_table⁄ | |
page_fluid⁄panel_title⁄h2⁄br⁄div⁄app_ui⁄Inputs⁄Outputs⁄Sessionrj | |
èäê}®BØM™M¿C®M—,H‘,Hà—I‘Iÿ | |
èäê[—!‘!ÿ | |
èäò | |
—&‘&Ò Ù | |
èäÿáNÇNê2ó5í5–<¿]ê5—S‘S—T‘TÿáEÇEêÄE—‘ÿáFÇFà3–4ÄF—5‘5Ò | |
Ù | |
ÄMê& | |
Äcà&ê&—‘ÄÄÄr" | |
a | |
je | |
†e | |
j | |
dddç°e | |
†de | |
jded d | |
çdç°dd | |
dçZe | |
†e | |
†e | |
jdddç°e | |
jddçe | |
jeddç°ZeeedúddÑZeeeÉZdS | |
date_rangeZcumsum⁄plot)⁄tsr | |
plotnine.datar | |
brush_opts⁄ | |
page_fluid⁄panel_title⁄h2⁄br⁄divZapp_ui⁄Inputs⁄Outputs⁄Sessionr% | |
˝ | |
import sys | |
if "pyodide" in sys.modules: | |
# psutil doesn't work on pyodide--use fake data instead | |
from fakepsutil import cpu_count, cpu_percent | |
else: | |
from psutil import cpu_count, cpu_percent | |
from math import ceil | |
import matplotlib | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import pandas as pd | |
from shiny import App, Inputs, Outputs, Session, reactive, render, ui | |
# The agg matplotlib backend seems to be a little more efficient than the default when | |
# running on macOS, and also gives more consistent results across operating systems | |
matplotlib.use("agg") | |
# max number of samples to retain | |
MAX_SAMPLES = 1000 | |
# secs between samples | |
SAMPLE_PERIOD = 1 | |
ncpu = cpu_count(logical=True) | |
app_ui = ui.page_fluid( | |
ui.tags.style( | |
""" | |
/* Don't apply fade effect, it's constantly recalculating */ | |
.recalculating { | |
opacity: 1; | |
} | |
tbody > tr:last-child { | |
/*border: 3px solid var(--bs-dark);*/ | |
box-shadow: | |
0 0 2px 1px #fff, /* inner white */ | |
0 0 4px 2px #0ff, /* middle cyan */ | |
0 0 5px 3px #00f; /* outer blue */ | |
} | |
#table table { | |
table-layout: fixed; | |
width: %s; | |
font-size: 0.8em; | |
} | |
th, td { | |
text-align: center; | |
} | |
""" | |
% f"{ncpu*4}em" | |
), | |
ui.h3("CPU Usage %", class_="mt-2"), | |
ui.layout_sidebar( | |
ui.panel_sidebar( | |
ui.input_select( | |
"cmap", | |
"Colormap", | |
{ | |
"inferno": "inferno", | |
"viridis": "viridis", | |
"copper": "copper", | |
"prism": "prism (not recommended)", | |
}, | |
), | |
ui.p(ui.input_action_button("reset", "Clear history", class_="btn-sm")), | |
ui.input_switch("hold", "Freeze output", value=False), | |
class_="mb-3", | |
), | |
ui.panel_main( | |
ui.div( | |
{"class": "card mb-3"}, | |
ui.div( | |
{"class": "card-body"}, | |
ui.h5({"class": "card-title mt-0"}, "Graphs"), | |
ui.output_plot("plot", height=f"{ncpu * 40}px"), | |
), | |
ui.div( | |
{"class": "card-footer"}, | |
ui.input_numeric("sample_count", "Number of samples per graph", 50), | |
), | |
), | |
ui.div( | |
{"class": "card"}, | |
ui.div( | |
{"class": "card-body"}, | |
ui.h5({"class": "card-title m-0"}, "Heatmap"), | |
), | |
ui.div( | |
{"class": "card-body overflow-auto pt-0"}, | |
ui.output_table("table"), | |
), | |
ui.div( | |
{"class": "card-footer"}, | |
ui.input_numeric("table_rows", "Rows to display", 5), | |
), | |
), | |
), | |
), | |
) | |
@reactive.Calc | |
def cpu_current(): | |
reactive.invalidate_later(SAMPLE_PERIOD) | |
return cpu_percent(percpu=True) | |
def server(input: Inputs, output: Outputs, session: Session): | |
cpu_history = reactive.Value(None) | |
@reactive.Calc | |
def cpu_history_with_hold(): | |
# If "hold" is on, grab an isolated snapshot of cpu_history; if not, then do a | |
# regular read | |
if not input.hold(): | |
return cpu_history() | |
else: | |
# Even if frozen, we still want to respond to input.reset() | |
input.reset() | |
with reactive.isolate(): | |
return cpu_history() | |
@reactive.Effect | |
def collect_cpu_samples(): | |
"""cpu_percent() reports just the current CPU usage sample; this Effect gathers | |
them up and stores them in the cpu_history reactive value, in a numpy 2D array | |
(rows are CPUs, columns are time).""" | |
new_data = np.vstack(cpu_current()) | |
with reactive.isolate(): | |
if cpu_history() is None: | |
cpu_history.set(new_data) | |
else: | |
combined_data = np.hstack([cpu_history(), new_data]) | |
# Throw away extra data so we don't consume unbounded amounts of memory | |
if combined_data.shape[1] > MAX_SAMPLES: | |
combined_data = combined_data[:, -MAX_SAMPLES:] | |
cpu_history.set(combined_data) | |
@reactive.Effect(priority=100) | |
@reactive.event(input.reset) | |
def reset_history(): | |
cpu_history.set(None) | |
@output | |
@render.plot | |
def plot(): | |
history = cpu_history_with_hold() | |
if history is None: | |
history = np.array([]) | |
history.shape = (ncpu, 0) | |
nsamples = input.sample_count() | |
# Throw away samples too old to fit on the plot | |
if history.shape[1] > nsamples: | |
history = history[:, -nsamples:] | |
ncols = 2 | |
nrows = int(ceil(ncpu / ncols)) | |
fig, axeses = plt.subplots( | |
nrows=nrows, | |
ncols=ncols, | |
squeeze=False, | |
) | |
for i in range(0, ncols * nrows): | |
row = i // ncols | |
col = i % ncols | |
axes = axeses[row, col] | |
if i >= len(history): | |
axes.set_visible(False) | |
continue | |
data = history[i] | |
axes.yaxis.set_label_position("right") | |
axes.yaxis.tick_right() | |
axes.set_xlim(-(nsamples - 1), 0) | |
axes.set_ylim(0, 100) | |
assert len(data) <= nsamples | |
# Set up an array of x-values that will right-align the data relative to the | |
# plotting area | |
x = np.arange(0, len(data)) | |
x = np.flip(-x) | |
# Color bars by cmap | |
color = plt.get_cmap(input.cmap())(data / 100) | |
axes.bar(x, data, color=color, linewidth=0, width=1.0) | |
axes.set_yticks([25, 50, 75]) | |
for ytl in axes.get_yticklabels(): | |
if col == ncols - 1 or i == ncpu - 1 or True: | |
ytl.set_fontsize(7) | |
else: | |
ytl.set_visible(False) | |
hide_ticks(axes.yaxis) | |
for xtl in axes.get_xticklabels(): | |
xtl.set_visible(False) | |
hide_ticks(axes.xaxis) | |
axes.grid(True, linewidth=0.25) | |
return fig | |
@output | |
@render.table | |
def table(): | |
history = cpu_history_with_hold() | |
latest = pd.DataFrame(history).transpose().tail(input.table_rows()) | |
if latest.shape[0] == 0: | |
return latest | |
return ( | |
latest.style.format(precision=0) | |
.hide(axis="index") | |
.set_table_attributes( | |
'class="dataframe shiny-table table table-borderless font-monospace"' | |
) | |
.background_gradient(cmap=input.cmap(), vmin=0, vmax=100) | |
) | |
def hide_ticks(axis): | |
for ticks in [axis.get_major_ticks(), axis.get_minor_ticks()]: | |
for tick in ticks: | |
tick.tick1line.set_visible(False) | |
tick.tick2line.set_visible(False) | |
tick.label1.set_visible(False) | |
tick.label2.set_visible(False) | |
app = App(app_ui, server) | |
"""Generates synthetic data""" | |
import numpy as np | |
def cpu_count(logical: bool = True): | |
return 8 if logical else 4 | |
last_sample = np.random.uniform(0, 100, size=cpu_count(True)) | |
def cpu_percent(interval=None, percpu=False): | |
global last_sample | |
delta = np.random.normal(scale=10, size=len(last_sample)) | |
last_sample = (last_sample + delta).clip(0, 100) | |
if percpu: | |
return last_sample.tolist() | |
else: | |
return last_sample.mean() | |
ß | |
d¶ | |
e | |
defdÑZ | |
The first time you click the button, you should see a 1 appear below the button, | |
as well as 2 messages in the python console (all reporting 1 click). After | |
clicking once, clicking again should increment the number below the button and | |
print the number of clicks in the console twice. | |
⁄Sync⁄btnzClick me⁄ btn_value⁄Async⁄ btn_async⁄btn_async_value⁄input⁄output⁄sessionc | |
Nc | |
–!•3†s†s°u§u°:§:—.‘.–.–.–.r | |
–'≠®UØ_™_—->‘->—)?‘)?—@‘@–@–@–@r | |
–'≠®S©¨—2‘2–2–2–2r | |
àeïcò#ëhîh——‘–›ê3âxåxàr | |
Ñ_› | |
á^Ç^êEîI—‘5 | |
á^Ç^êEîI—‘ï | |
á^Ç^êEîI—‘ | |
á^Ç^êEîO—$‘$A | |
á^Ç^êEîO—$‘$!ùs | |
á^Ç^êK— ‘ | |
page_fluid⁄p⁄navset_tab_card⁄nav⁄input_action_button⁄ output_ui⁄app_ui⁄Inputs⁄Outputs⁄Sessionr- | |
ÄDÑF | |
ÒÙ | |
èäÿÿ◊"“"†5®*—5‘5ÿèLäLò—%‘%Ò | |
Ù | |
| |
—;‘;ÿèLäL–*—+‘+Ò | |
Ù | |
Ò | |
Ù | |
Ù | |
Ä0/ê& | |
Äcà&ê&—‘ÄÄÄr | |
import asyncio | |
from shiny import * | |
from shiny.ui import tags | |
app_ui = ui.page_fluid( | |
tags.p( | |
""" | |
The first time you click the button, you should see a 1 appear below the button, | |
as well as 2 messages in the python console (all reporting 1 click). After | |
clicking once, clicking again should increment the number below the button and | |
print the number of clicks in the console twice. | |
""" | |
), | |
ui.navset_tab_card( | |
ui.nav( | |
"Sync", | |
ui.input_action_button("btn", "Click me"), | |
ui.output_ui("btn_value"), | |
), | |
ui.nav( | |
"Async", | |
ui.input_action_button("btn_async", "Click me"), | |
ui.output_ui("btn_async_value"), | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.btn) | |
def _(): | |
print("@effect() event: ", str(input.btn())) | |
@reactive.Calc | |
@reactive.event(input.btn) | |
def btn() -> int: | |
return input.btn() | |
@reactive.Effect | |
def _(): | |
print("@calc() event: ", str(btn())) | |
@output | |
@render.ui | |
@reactive.event(input.btn) | |
def btn_value(): | |
return str(input.btn()) | |
# ----------------------------------------------------------------------------- | |
# Async | |
# ----------------------------------------------------------------------------- | |
@reactive.Effect | |
@reactive.event(input.btn_async) | |
async def _(): | |
await asyncio.sleep(0) | |
print("async @effect() event: ", str(input.btn_async())) | |
@reactive.Calc | |
@reactive.event(input.btn_async) | |
async def btn_async_r() -> int: | |
await asyncio.sleep(0) | |
return input.btn_async() | |
@reactive.Effect | |
async def _(): | |
val = await btn_async_r() | |
print("async @calc() event: ", str(val)) | |
@output | |
@render.ui | |
@reactive.event(btn_async_r) | |
async def btn_async_value(): | |
val = await btn_async_r() | |
print("== " + str(val)) | |
return str(val) | |
app = App(app_ui, server) | |
ß | |
¶ | |
edefdÑZ | |
page_fluid⁄input_checkbox⁄panel_conditional⁄tags⁄h5⁄output_plot⁄app_uir | |
åè | |
ä | |
–F—G‘GÒÙ | |
åè | |
ä | |
–P—Q‘QÒÙ | |
Ù | |
Ä&ê& | |
Äcà&ê&—‘ÄÄÄr | |
import matplotlib.pyplot as plt | |
from shiny import App, Inputs, Outputs, Session, render, ui | |
app_ui = ui.page_fluid( | |
ui.input_checkbox("render", "Render", value=True), | |
ui.panel_conditional( | |
"input.render", | |
ui.tags.h5("A plot should appear immediately below this text."), | |
), | |
ui.output_plot("mpl"), | |
ui.panel_conditional( | |
"input.render", | |
ui.tags.h5("An error message should appear immediately below this text."), | |
), | |
ui.output_plot("mpl_bad"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot | |
def mpl(): | |
if input.render(): | |
plt.hist([1, 1, 2, 3, 5]) | |
@output | |
@render.plot | |
async def mpl_bad(): | |
if input.render(): | |
plt.hist([1, 1, 2, 3, 5]) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.h3("HTTP request headers"), | |
ui.output_text_verbatim("headers", placeholder=True), | |
ui.h3("User and groups"), | |
ui.output_text_verbatim("user_groups", placeholder=True), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def headers(): | |
s = "" | |
for key, value in session.http_conn.headers.items(): | |
s += f"{key}: {value}\n" | |
return s | |
@output | |
@render.text | |
def user_groups(): | |
return f"session.user: {session.user}\nsession.groups: {session.groups}" | |
app = App(app_ui, server) | |
from datetime import date | |
from shiny import * | |
from shiny.ui import h2, tags | |
app_ui = ui.page_fluid( | |
ui.panel_title("Changing the values of inputs from the server"), | |
ui.row( | |
ui.column( | |
4, | |
ui.panel_well( | |
tags.h4("These inputs control the other inputs on the page"), | |
ui.input_text( | |
"control_label", "This controls some of the labels:", "LABEL TEXT" | |
), | |
ui.input_slider( | |
"control_num", "This controls values:", min=1, max=20, value=15 | |
), | |
), | |
), | |
ui.column( | |
4, | |
ui.panel_well( | |
tags.h4("These inputs are controlled by the other inputs"), | |
ui.input_text("inText", "Text input:", value="start text"), | |
ui.input_numeric( | |
"inNumber", "Number input:", min=1, max=20, value=5, step=0.5 | |
), | |
ui.input_numeric( | |
"inNumber2", "Number input 2:", min=1, max=20, value=5, step=0.5 | |
), | |
ui.input_slider("inSlider", "Slider input:", min=1, max=20, value=15), | |
ui.input_slider( | |
"inSlider2", "Slider input 2:", min=1, max=20, value=(5, 15) | |
), | |
ui.input_slider( | |
"inSlider3", "Slider input 3:", min=1, max=20, value=(5, 15) | |
), | |
ui.input_date("inDate", "Date input:"), | |
ui.input_date_range("inDateRange", "Date range input:"), | |
), | |
), | |
ui.column( | |
4, | |
ui.panel_well( | |
ui.input_checkbox("inCheckbox", "Checkbox input", value=False), | |
ui.input_checkbox_group( | |
"inCheckboxGroup", | |
"Checkbox group input:", | |
{ | |
"option1": "label 1", | |
"option2": "label 2", | |
}, | |
), | |
ui.input_radio_buttons( | |
"inRadio", | |
"Radio buttons:", | |
{ | |
"option1": "label 1", | |
"option2": "label 2", | |
}, | |
), | |
ui.input_select( | |
"inSelect", | |
"Select input:", | |
{ | |
"option1": "label 1", | |
"option2": "label 2", | |
}, | |
), | |
ui.input_select( | |
"inSelect2", | |
"Select input 2:", | |
{ | |
"option1": "label 1", | |
"option2": "label 2", | |
}, | |
multiple=True, | |
), | |
), | |
ui.navset_tab( | |
ui.nav("panel1", h2("This is the first panel.")), | |
ui.nav("panel2", h2("This is the second panel.")), | |
id="inTabset", | |
), | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
# We'll use these multiple times, so use short var names for | |
# convenience. | |
c_label = input.control_label() | |
c_num = input.control_num() | |
# Text ===================================================== | |
# Change both the label and the text | |
ui.update_text( | |
"inText", | |
label="New " + c_label, | |
value="New text " + str(c_num), | |
) | |
# Number =================================================== | |
# Change the value | |
ui.update_numeric("inNumber", value=c_num) | |
# Change the label, value, min, and max | |
ui.update_numeric( | |
"inNumber2", | |
label="Number " + c_label, | |
value=c_num, | |
min=c_num - 10, | |
max=c_num + 10, | |
step=5, | |
) | |
# Slider input ============================================= | |
# Only label and value can be set for slider | |
ui.update_slider("inSlider", label="Slider " + c_label, value=c_num) | |
# Slider range input ======================================= | |
# For sliders that pick out a range, pass in a vector of 2 | |
# values. | |
ui.update_slider("inSlider2", value=(c_num - 1, c_num + 1)) | |
# Only change the upper handle | |
ui.update_slider("inSlider3", value=(input.inSlider3()[0], c_num + 2)) | |
# Date input =============================================== | |
# Only label and value can be set for date input | |
ui.update_date("inDate", label="Date " + c_label, value=date(2013, 4, c_num)) | |
# Date range input ========================================= | |
# Only label and value can be set for date range input | |
ui.update_date_range( | |
"inDateRange", | |
label="Date range " + c_label, | |
start=date(2013, 1, c_num), | |
end=date(2013, 12, c_num), | |
min=date(2001, 1, c_num), | |
max=date(2030, 1, c_num), | |
) | |
# # Checkbox =============================================== | |
ui.update_checkbox("inCheckbox", value=c_num % 2) | |
# Checkbox group =========================================== | |
# Create a list of new options, where the name of the items | |
# is something like 'option label x A', and the values are | |
# 'option-x-A'. | |
opt_labels = [f"option label {c_num} {type}" for type in ["A", "B"]] | |
opt_vals = [f"option-{c_num}-{type}" for type in ["A", "B"]] | |
opts_dict = dict(zip(opt_vals, opt_labels)) | |
# Set the label, choices, and selected item | |
ui.update_checkbox_group( | |
"inCheckboxGroup", | |
label="Checkbox group " + c_label, | |
choices=opts_dict, | |
selected=f"option-{c_num}-A", | |
) | |
# Radio group ============================================== | |
ui.update_radio_buttons( | |
"inRadio", | |
label="Radio " + c_label, | |
choices=opts_dict, | |
selected=f"option-{c_num}-A", | |
) | |
# Select input ============================================= | |
# Create a list of new options, where the name of the items | |
# is something like 'option label x A', and the values are | |
# 'option-x-A'. | |
ui.update_select( | |
"inSelect", | |
label="Select " + c_label, | |
choices=opts_dict, | |
selected=f"option-{c_num}-A", | |
) | |
# Can also set the label and select an item (or more than | |
# one if it's a multi-select) | |
ui.update_select( | |
"inSelect2", | |
label="Select label " + c_label, | |
choices=opts_dict, | |
selected=f"option-{c_num}-B", | |
) | |
# Tabset input ============================================= | |
# Change the selected tab. | |
# The tabsetPanel must have been created with an 'id' argument | |
ui.update_navs("inTabset", selected="panel2" if c_num % 2 else "panel1") | |
app = App(app_ui, server, debug=True) | |
import starlette.responses | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.markdown( | |
""" | |
## Sticky load balancing test | |
The purpose of this app is to determine if HTTP requests made by the client are | |
correctly routed back to the same Python process where the session resides. It | |
is only useful for testing deployments that load balance traffic across more | |
than one Python process. | |
If this test fails, it means that sticky load balancing is not working, and | |
certain Shiny functionality (like file upload/download or server-side selectize) | |
are likely to randomly fail. | |
""" | |
), | |
ui.tags.div( | |
{"class": "card"}, | |
ui.tags.div( | |
{"class": "card-body font-monospace"}, | |
ui.tags.div("Attempts: ", ui.tags.span("0", id="count")), | |
ui.tags.div("Status: ", ui.tags.span(id="status")), | |
ui.output_ui("out"), | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def out(): | |
# Register a dynamic route for the client to try to connect to. | |
# It does nothing, just the 200 status code is all that the client | |
# will care about. | |
url = session.dynamic_route( | |
"test", | |
lambda req: starlette.responses.PlainTextResponse( | |
"OK", headers={"Cache-Control": "no-cache"} | |
), | |
) | |
# Send JS code to the client to repeatedly hit the dynamic route. | |
# It will succeed if and only if we reach the correct Python | |
# process. | |
return ui.tags.script( | |
f""" | |
const url = "{url}"; | |
const count_el = document.getElementById("count"); | |
const status_el = document.getElementById("status"); | |
let count = 0; | |
async function check_url() {{ | |
count_el.innerHTML = ++count; | |
try {{ | |
const resp = await fetch(url); | |
if (!resp.ok) {{ | |
status_el.innerHTML = "Failure!"; | |
return; | |
}} else {{ | |
status_el.innerHTML = "In progress"; | |
}} | |
}} catch(e) {{ | |
status_el.innerHTML = "Failure!"; | |
return; | |
}} | |
if (count === 100) {{ | |
status_el.innerHTML = "Test complete"; | |
return; | |
}} | |
setTimeout(check_url, 10); | |
}} | |
check_url(); | |
""" | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
# ============================================================ | |
# Counter module | |
# ============================================================ | |
@module.ui | |
def counter_ui(label: str = "Increment counter") -> ui.TagChildArg: | |
return ui.div( | |
{"style": "border: 1px solid #ccc; border-radius: 5px; margin: 5px 0;"}, | |
ui.h2("This is " + label), | |
ui.input_action_button(id="button", label=label), | |
ui.output_text_verbatim(id="out"), | |
) | |
@module.server | |
def counter_server( | |
input: Inputs, output: Outputs, session: Session, starting_value: int = 0 | |
): | |
count: reactive.Value[int] = reactive.Value(starting_value) | |
@reactive.Effect | |
@reactive.event(input.button) | |
def _(): | |
count.set(count() + 1) | |
@output | |
@render.text | |
def out() -> str: | |
return f"Click count is {count()}" | |
# ============================================================ | |
# Counter Wrapper module -- shows that counter still works | |
# the same way when wrapped in a dynamic UI | |
# ============================================================ | |
@module.ui | |
def counter_wrapper_ui() -> ui.TagChildArg: | |
return ui.output_ui("dynamic_counter") | |
@module.server | |
def counter_wrapper_server( | |
input: Inputs, output: Outputs, session: Session, label: str = "Increment counter" | |
): | |
@output() | |
@render.ui() | |
def dynamic_counter(): | |
return counter_ui("counter", label) | |
counter_server("counter") | |
# ============================================================================= | |
# App that uses module | |
# ============================================================================= | |
app_ui = ui.page_fluid( | |
counter_ui("counter1", "Counter 1"), | |
counter_wrapper_ui("counter2_wrapper"), | |
ui.output_ui("counter3_ui"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
counter_server("counter1") | |
counter_wrapper_server("counter2_wrapper", "Counter 2") | |
@output() | |
@render.ui() | |
def counter3_ui(): | |
counter_server("counter3") | |
return counter_ui("counter3", "Counter 3") | |
counter_server("counter") | |
app = App(app_ui, server) | |
from shiny import * | |
from shiny.types import SafeException | |
app_ui = ui.page_fluid( | |
ui.input_action_button("safe", "Throw a safe error"), | |
ui.output_ui("safe"), | |
ui.input_action_button("unsafe", "Throw an unsafe error"), | |
ui.output_ui("unsafe"), | |
ui.input_text( | |
"txt", | |
"Enter some text below, then remove it. Notice how the text is never fully removed.", | |
), | |
ui.output_ui("txt_out"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Calc | |
def safe_click(): | |
req(input.safe()) | |
return input.safe() | |
@output | |
@render.ui | |
def safe(): | |
raise SafeException(f"You've clicked {str(safe_click())} times") | |
@output | |
@render.ui | |
def unsafe(): | |
req(input.unsafe()) | |
raise Exception(f"Super secret number of clicks: {str(input.unsafe())}") | |
@reactive.Effect | |
def _(): | |
req(input.unsafe()) | |
print("unsafe clicks:", input.unsafe()) | |
# raise Exception("Observer exception: this should cause a crash") | |
@output | |
@render.ui | |
def txt_out(): | |
req(input.txt(), cancel_output=True) | |
return input.txt() | |
app = App(app_ui, server) | |
app.sanitize_errors = True | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import pandas as pd | |
from sklearn.cluster import KMeans | |
from shiny import App, reactive, render, server, ui | |
# Load the data | |
df = pd.read_csv( | |
"https://raw.githubusercontent.com/rstudio/shiny-examples/main/050-kmeans-example/faithful.csv" | |
) | |
# Define the UI | |
app_ui = ui.page( | |
ui.title_panel("K-Means Clustering"), | |
ui.sidebar_panel( | |
ui.select_input("features", "Select features:", choices=list(df.columns[1:3])), | |
ui.numeric_input("clusters", "Number of clusters:", value=2, min=1, max=9), | |
), | |
ui.main_panel(ui.plot_output("plot")), | |
) | |
# Define the server logic | |
def app_server(input, output, session): | |
# Define the reactive expression to generate the plot | |
@reactive | |
def plot_data(): | |
# Subset the data based on the selected features | |
data = df.loc[:, [input.features, "waiting"]] | |
# Run K-Means clustering | |
kmeans = KMeans(n_clusters=input.clusters) | |
kmeans.fit(data) | |
# Add the cluster assignments to the data frame | |
data["cluster"] = kmeans.labels_ | |
# Create the scatter plot | |
fig, ax = plt.subplots() | |
ax.scatter( | |
data[input.features], data["waiting"], c=data["cluster"], cmap="Set1" | |
) | |
ax.set_xlabel(input.features) | |
ax.set_ylabel("Waiting time") | |
ax.set_title("K-Means Clustering") | |
plt.close(fig) | |
return fig | |
# Render the plot | |
@output | |
@render.plot(width="100%", height="500px") | |
def plot(): | |
return plot_data() | |
# Run the app | |
app = App(app_ui, app_server) | |
ß | |
Z | |
dúe† | |
j | |
j | |
j | |
X variable⁄wt)⁄choices⁄selected⁄yz | |
Y variable⁄mpg⁄colorzColor variable⁄cyl⁄Seaborn⁄seabornr | |
ó | |
|t | |
fdѶ | |
d¨¶ | |
N)È | |
linewidths)⁄plt⁄subplots⁄sns⁄scatterplot⁄histplot⁄kdeplot)r | |
¨¶ | |
¶ | |
facet_wrap⁄ | |
geom_point⁄ggplot⁄stat_smooth⁄theme⁄theme_bw⁄int64zfactor(˙))r | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
‡àk†–&—&‘&Ò | |
' | |
" | |
% | |
+ | |
r8 | |
date_range⁄cumsum⁄plot)⁄tss | |
Ò | |
Ù | |
à | |
matplotlib)⁄backend)r! | |
¶ | |
sort_indexri | |
èäò | |
®Dà—1‘1–1ÿ | |
◊“†–—&‘&–&ÿèzäzâ|å|–r8 | |
collisionss | |
Ù | |
à | |
| |
Ñ]7 | |
| |
| |
| |
Ò | |
. | |
plotnine.datar | |
page_fluid⁄panel_title⁄h2⁄br⁄app_ui⁄Inputs⁄Outputs⁄Sessionrõ | |
èäêz—"‘"ÿ | |
èäÿ5¿–L–LÿèOäOÿê\®4®4∞∞¥± | |
¥ | |
—+>‘+>» | |
Ò | |
Ù | |
| |
èOäOÿê\®4®4∞∞¥± | |
¥ | |
—+>‘+>» | |
Ò | |
Ù | |
| |
èOäOÿÿ ÿòò[òVú[ô]ú]—+‘+ÿ | |
Ò | |
Ù | |
Ò | |
Ù | |
ÒÙ | |
èäêy—!‘!ÿ | |
èäÿ | |
–5–6ÿèOäOòE†:∞3∏B¿aàO—H‘HÿèOäOòE†=∞a∏Q¿càO—J‘JÒ | |
Ù | |
ÒÙ | |
èäÿáNÇNê2ó5í5–<¿]ê5—S‘S—T‘TÿáEÇEêÄE—‘ÿáFÇFà3–4ÄF—5‘5Ò | |
Ù | |
Ä\3ê& | |
Äcà&ê&—‘ÄÄÄr8 | |
import matplotlib.pyplot as plt | |
import numpy as np | |
import pandas as pd | |
import seaborn as sns | |
from plotnine.data import mtcars | |
from shiny import * | |
nav = ui.navset_pill_list( | |
ui.nav_control(ui.p("Choose a package", class_="lead text-center")), | |
ui.nav( | |
"Plotnine", | |
ui.output_plot("plotnine"), | |
ui.div( | |
{"class": "d-flex justify-content-center", "style": "gap: 5rem"}, | |
ui.input_select( | |
"x", "X variable", choices=list(mtcars.keys()), selected="wt" | |
), | |
ui.input_select( | |
"y", "Y variable", choices=list(mtcars.keys()), selected="mpg" | |
), | |
ui.input_select( | |
"color", | |
"Color variable", | |
choices=list(mtcars.keys()), | |
selected="cyl", | |
), | |
), | |
), | |
ui.nav( | |
"Seaborn", | |
ui.output_plot("seaborn"), | |
ui.div( | |
{"class": "d-flex justify-content-around"}, | |
ui.input_slider("var", "Variance", min=0.1, max=10, value=2), | |
ui.input_slider("cov", "Co-variance", min=0, max=1, value=0.4), | |
), | |
), | |
ui.nav("Pandas", ui.output_plot("pandas")), | |
ui.nav("Holoviews", ui.output_plot("holoviews", height="600px")), | |
ui.nav("xarray", ui.output_plot("xarray")), | |
ui.nav("geopandas", ui.output_plot("geopandas")), | |
ui.nav("missingno", ui.output_plot("missingno")), | |
widths=(2, 10), | |
well=False, | |
) | |
app_ui = ui.page_fluid( | |
ui.panel_title(ui.h2("Py-Shiny static plotting examples", class_="text-center")), | |
ui.br(class_="py-3"), | |
ui.div(nav, style="max-width: 90%; margin: auto"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Calc | |
def fake_data(): | |
n = 5000 | |
mean = [0, 0] | |
rng = np.random.RandomState(0) | |
cov = [(input.var(), input.cov()), (input.cov(), 1 / input.var())] | |
return rng.multivariate_normal(mean, cov, n).T | |
@output | |
@render.plot | |
def seaborn(): | |
x, y = fake_data() | |
f, ax = plt.subplots(figsize=(6, 6)) | |
sns.scatterplot(x=x, y=y, s=5, color=".15") | |
sns.histplot(x=x, y=y, bins=50, pthresh=0.1, cmap="mako") | |
sns.kdeplot(x=x, y=y, levels=5, color="w", linewidths=1) | |
return f | |
@output | |
@render.plot | |
def plotnine(): | |
from plotnine import ( | |
aes, | |
facet_wrap, | |
geom_point, | |
ggplot, | |
stat_smooth, | |
theme, | |
theme_bw, | |
) | |
color_var = input.color() | |
if str(mtcars[color_var].dtype) == "int64": | |
color_var = f"factor({color_var})" | |
return ( | |
ggplot(mtcars, aes(input.x(), input.y(), color=color_var)) | |
+ geom_point() | |
+ stat_smooth(method="lm") | |
+ facet_wrap("~gear") | |
+ theme_bw(base_size=16) | |
+ theme(legend_position="top") | |
) | |
@output | |
@render.plot | |
def pandas(): | |
ts = pd.Series( | |
np.random.randn(1000), index=pd.date_range("1/1/2000", periods=1000) | |
) | |
ts = ts.cumsum() | |
return ts.plot() | |
@output | |
@render.plot | |
def holoviews(): | |
import holoviews as hv | |
from bokeh.sampledata.les_mis import data as les_mis | |
links = pd.DataFrame(les_mis["links"]) | |
return hv.render(hv.Chord(links), backend="matplotlib") | |
@output | |
@render.plot | |
def xarray(): | |
import xarray as xr | |
airtemps = xr.tutorial.open_dataset("air_temperature") | |
air = airtemps.air - 273.15 | |
air.attrs = airtemps.air.attrs | |
air.attrs["units"] = "deg C" | |
return air.isel(lon=10, lat=[19, 21, 22]).plot.line(x="time") | |
@output | |
@render.plot | |
def geopandas(): | |
import geopandas | |
nybb_path = geopandas.datasets.get_path("nybb") | |
boros = geopandas.read_file(nybb_path) | |
boros.set_index("BoroCode", inplace=True) | |
boros.sort_index(inplace=True) | |
return boros.plot() | |
@output | |
@render.plot | |
def missingno(): | |
import missingno as msno | |
collisions = pd.read_csv( | |
"https://raw.githubusercontent.com/ResidentMario/missingno-data/master/nyc_collision_factors.csv" | |
) | |
return msno.matrix(collisions.sample(250)) | |
app = App(app_ui, server) | |
# pyright: reportUnusedFunction=false | |
import typing | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_numeric("n", "N", 20), | |
ui.input_numeric("n2", "N2", 50), | |
ui.input_checkbox("checkbox", "Checkbox", True), | |
ui.output_text_verbatim("txt", placeholder=True), | |
ui.output_text_verbatim("txt2", placeholder=True), | |
ui.output_text_verbatim("txt3", placeholder=True), | |
) | |
# By default the type of any input value, like input.n(), is Any, so no type checking | |
# will be used. | |
# | |
# But it is possible to specify the type of the input value, by creating a subclass of | |
# Inputs. We'll do that for input.n2() and input.checkbox(): | |
class ShinyInputs(Inputs): | |
n2: reactive.Value[int] | |
check: reactive.Value[bool] | |
def server(input: Inputs, output: Outputs, session: Session): | |
# Cast `input` to our ShinyInputs class. This just tells the static type checker | |
# that we want it treated as a ShinyInputs object for type checking; it has no | |
# run-time effect. | |
input = typing.cast(ShinyInputs, input) | |
# The type checker knows that r() returns an int, which you can see if you hover | |
# over it. | |
@reactive.Calc | |
def r(): | |
if input.n() is None: | |
return 0 | |
return input.n() * 2 | |
# Because we did NOT add a type for input.n (we did input.n2), the type checker | |
# thinks the return type of input.n() is Any, so we don't get type checking here. | |
# The function is returning the wrong value here: it returns an int instead of a | |
# string, but this error is not flagged. | |
@output | |
@render.text | |
async def txt(): | |
return input.n() * 2 | |
# In contrast, input.n2() is declared to return an int, so the type check does flag | |
# this error -- the `render.text()` is underlined in red. | |
@output | |
@render.text | |
async def txt2(): | |
return input.n2() * 2 | |
# This is a corrected version of the function above. It returns a string, and is not | |
# marked in red. | |
@output | |
@render.text | |
async def txt3(): | |
return str(input.n2() * 2) | |
app = App(app_ui, server) | |
from datetime import datetime | |
from starlette.requests import Request | |
from shiny import App, reactive, render, ui | |
def app_ui(request: Request): | |
return ui.page_fluid( | |
ui.div("This page was rendered at ", datetime.now().isoformat()), | |
ui.output_text("now"), | |
) | |
def server(input, output, session): | |
@output | |
@render.text | |
def now(): | |
reactive.invalidate_later(0.1) | |
return f"The current time is {datetime.now().isoformat()}" | |
app = App(app_ui, server) | |
import random | |
import time | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("first", "Invalidate first (slow) computation"), | |
ui.input_action_button("second", "Invalidate second (fast) computation"), | |
ui.br(), | |
ui.output_ui("result"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Calc | |
def first(): | |
input.first() | |
p = ui.Progress() | |
for i in range(30): | |
p.set(i / 30, message="Computing, please wait...") | |
time.sleep(0.1) | |
p.close() | |
return random.randint(1, 1000) | |
@reactive.Calc | |
def second(): | |
input.second() | |
return random.randint(1, 1000) | |
@output | |
@render.ui | |
def result(): | |
return first() + second() | |
app = App(app_ui, server) | |
from datetime import datetime | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("close", "Close the session"), | |
ui.p( | |
"""If this example is running on the browser (i.e., via shinylive), | |
closing the session will log a message to the JavaScript console | |
(open the browser's developer tools to see it). | |
""" | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
def log(): | |
print("Session ended at: " + datetime.now().strftime("%H:%M:%S")) | |
session.on_ended(log) | |
@reactive.Effect | |
@reactive.event(input.close) | |
async def _(): | |
await session.close() | |
app = App(app_ui, server) | |
import asyncio | |
from datetime import date | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.download_button("downloadData", "Download"), | |
) | |
# For more examples of different types of download handlers, see: | |
# https://github.com/rstudio/py-shiny/blob/68ffc27/examples/download/app.py#L90 | |
def server(input: Inputs, output: Outputs, session: Session): | |
@session.download( | |
filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100,999)}.csv" | |
) | |
async def downloadData(): | |
await asyncio.sleep(0.25) | |
yield "one,two,three\n" | |
yield "Êñ∞,1,2\n" | |
yield "Âûã,4,5\n" | |
app = App(app_ui, server) | |
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb | |
21,6,160,110,3.9,2.62,16.46,0,1,4,4 | |
21,6,160,110,3.9,2.875,17.02,0,1,4,4 | |
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 | |
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 | |
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 | |
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 | |
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 | |
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 | |
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 | |
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 | |
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 | |
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 | |
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 | |
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 | |
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 | |
10.4,8,460,215,3,5.424,17.82,0,0,3,4 | |
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 | |
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 | |
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 | |
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 | |
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 | |
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 | |
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 | |
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 | |
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 | |
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 | |
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 | |
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 | |
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 | |
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 | |
15,8,301,335,3.54,3.57,14.6,0,1,5,8 | |
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 | |
import asyncio | |
from datetime import date | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.download_link("downloadData", "Download"), | |
) | |
# For more examples of different types of download handlers, see: | |
# https://github.com/rstudio/py-shiny/blob/68ffc27/examples/download/app.py#L90 | |
def server(input: Inputs, output: Outputs, session: Session): | |
@session.download( | |
filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100,999)}.csv" | |
) | |
async def downloadData(): | |
await asyncio.sleep(0.25) | |
yield "one,two,three\n" | |
yield "Êñ∞,1,2\n" | |
yield "Âûã,4,5\n" | |
app = App(app_ui, server) | |
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb | |
21,6,160,110,3.9,2.62,16.46,0,1,4,4 | |
21,6,160,110,3.9,2.875,17.02,0,1,4,4 | |
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 | |
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 | |
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 | |
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 | |
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 | |
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 | |
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 | |
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 | |
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 | |
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 | |
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 | |
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 | |
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 | |
10.4,8,460,215,3,5.424,17.82,0,0,3,4 | |
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 | |
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 | |
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 | |
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 | |
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 | |
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 | |
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 | |
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 | |
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 | |
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 | |
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 | |
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 | |
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 | |
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 | |
15,8,301,335,3.54,3.57,14.6,0,1,5,8 | |
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 | |
import asyncio | |
import io | |
import os | |
from datetime import date | |
from typing import Any | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
from shiny.ui import div, p | |
def make_example(id: str, label: str, title: str, desc: str, extra: Any = None): | |
return ui.column( | |
4, | |
div( | |
{"class": "card mb-4"}, | |
div(title, class_="card-header"), | |
div( | |
{"class": "card-body"}, | |
p(desc, class_="card-text text-muted"), | |
extra, | |
ui.download_button(id, label, class_="btn-primary"), | |
), | |
), | |
) | |
app_ui = ui.page_fluid( | |
ui.row( | |
make_example( | |
"download1", | |
label="Download CSV", | |
title="Simple case", | |
desc="Downloads a pre-existing file, using its existing name on disk.", | |
), | |
), | |
ui.row( | |
make_example( | |
"download2", | |
label="Download plot", | |
title="Dynamic data generation", | |
desc="Downloads a PNG that's generated on the fly.", | |
extra=[ | |
ui.input_text("title", "Plot title", "Random scatter plot"), | |
ui.input_slider( | |
"num_points", "Number of data points", min=1, max=100, value=50 | |
), | |
], | |
), | |
), | |
ui.row( | |
make_example( | |
"download3", | |
"Download", | |
"Dynamic filename", | |
"Demonstrates that filenames can be generated on the fly (and use Unicode characters!).", | |
), | |
), | |
ui.row( | |
make_example( | |
"download4", | |
"Download", | |
"Failed downloads", | |
"Throws an error in the download handler, download should not succeed.", | |
), | |
), | |
ui.row( | |
make_example( | |
"download5", | |
"Download", | |
"Undefined download", | |
"This button doesn't have corresponding server code registered to it, download should result in 404 error", | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@session.download() | |
def download1(): | |
""" | |
This is the simplest case. The implementation simply returns the name of a file. | |
Note that the function name (`download1`) determines which download_button() | |
corresponds to this function. | |
""" | |
path = os.path.join(os.path.dirname(__file__), "mtcars.csv") | |
return path | |
@session.download(filename="image.png") | |
def download2(): | |
""" | |
Another way to implement a file download is by yielding bytes; either all at | |
once, like in this case, or by yielding multiple times. When using this | |
approach, you should pass a filename argument to @session.download, which | |
determines what the browser will name the downloaded file. | |
""" | |
print(input.num_points()) | |
x = np.random.uniform(size=input.num_points()) | |
y = np.random.uniform(size=input.num_points()) | |
plt.figure() | |
plt.scatter(x, y) | |
plt.title(input.title()) | |
with io.BytesIO() as buf: | |
plt.savefig(buf, format="png") | |
yield buf.getvalue() | |
@session.download( | |
filename=lambda: f"新型-{date.today().isoformat()}-{np.random.randint(100,999)}.csv" | |
) | |
async def download3(): | |
await asyncio.sleep(0.25) | |
yield "one,two,three\n" | |
yield "Êñ∞,1,2\n" | |
yield "Âûã,4,5\n" | |
@session.download(id="download4", filename="failuretest.txt") | |
async def _(): | |
yield "hello" | |
raise Exception("This error was caused intentionally") | |
app = App(app_ui, server) | |
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb | |
21,6,160,110,3.9,2.62,16.46,0,1,4,4 | |
21,6,160,110,3.9,2.875,17.02,0,1,4,4 | |
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 | |
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 | |
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 | |
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 | |
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 | |
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 | |
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 | |
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 | |
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 | |
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 | |
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 | |
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 | |
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 | |
10.4,8,460,215,3,5.424,17.82,0,0,3,4 | |
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 | |
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 | |
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 | |
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 | |
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 | |
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 | |
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 | |
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 | |
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 | |
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 | |
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 | |
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 | |
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 | |
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 | |
15,8,301,335,3.54,3.57,14.6,0,1,5,8 | |
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 | |
from starlette.requests import Request | |
from starlette.responses import JSONResponse | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("serve", "Click to serve"), ui.div(id="messages") | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.serve) | |
def _(): | |
async def my_handler(request: Request) -> JSONResponse: | |
return JSONResponse({"n_clicks": input.serve()}, status_code=200) | |
path = session.dynamic_route("my_handler", my_handler) | |
print("Serving at: ", path) | |
ui.insert_ui( | |
ui.tags.script( | |
f""" | |
fetch('{path}') | |
.then(r => r.json()) | |
.then(x => {{ $('#messages').text(`Clicked ${{x.n_clicks}} times`); }}); | |
""" | |
), | |
selector="body", | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid(ui.input_action_button("btn", "Press me!")) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.btn) | |
def _(): | |
ui.insert_ui( | |
ui.p("Number of clicks: ", input.btn()), selector="#btn", where="afterEnd" | |
) | |
app = App(app_ui, server) | |
import random | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.markdown( | |
f""" | |
This example demonstrates how `@reactive.event()` can be used to restrict | |
execution of: (1) a `@render` function, (2) `@reactive.Calc`, or (3) | |
`@reactive.Effect`. | |
In all three cases, the output is dependent on a random value that gets updated | |
every 0.5 seconds (currently, it is {ui.output_ui("number", inline=True)}), but | |
the output is only updated when the button is clicked. | |
""" | |
), | |
ui.row( | |
ui.column( | |
3, | |
ui.input_action_button("btn_out", "(1) Update number"), | |
ui.output_text("out_out"), | |
), | |
ui.column( | |
3, | |
ui.input_action_button("btn_calc", "(2) Show 1 / number"), | |
ui.output_text("out_calc"), | |
), | |
ui.column( | |
3, | |
ui.input_action_button("btn_effect", "(3) Log number"), | |
ui.div(id="out_effect"), | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
# Update a random number every second | |
val = reactive.Value(random.randint(0, 1000)) | |
@reactive.Effect | |
def _(): | |
reactive.invalidate_later(0.5) | |
val.set(random.randint(0, 1000)) | |
# Always update this output when the number is updated | |
@output | |
@render.ui | |
def number(): | |
return val.get() | |
# Since ignore_none=False, the function executes before clicking the button. | |
# (input.btn_out() is 0 on page load, but @@reactive.event() treats 0 as None for | |
# action buttons.) | |
@output | |
@render.text | |
@reactive.event(input.btn_out, ignore_none=False) | |
def out_out(): | |
return str(val.get()) | |
@reactive.Calc | |
@reactive.event(input.btn_calc) | |
def calc(): | |
return 1 / val.get() | |
@output | |
@render.text | |
def out_calc(): | |
return str(calc()) | |
@reactive.Effect | |
@reactive.event(input.btn_effect) | |
def _(): | |
ui.insert_ui( | |
ui.p("Random number!", val.get()), | |
selector="#out_effect", | |
where="afterEnd", | |
) | |
app = App(app_ui, server) | |
import pathlib | |
import pandas as pd | |
from shiny import * | |
dir = pathlib.Path(__file__).parent | |
app_ui = ui.page_fluid(ui.output_table("result"), class_="p-3") | |
@reactive.file_reader(dir / "mtcars.csv") | |
def read_file(): | |
return pd.read_csv(dir / "mtcars.csv") | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.table | |
def result(): | |
return read_file() | |
app = App(app_ui, server) | |
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb | |
21,6,160,110,3.9,2.62,16.46,0,1,4,4 | |
21,6,160,110,3.9,2.875,17.02,0,1,4,4 | |
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 | |
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 | |
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 | |
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 | |
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 | |
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 | |
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 | |
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 | |
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 | |
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 | |
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 | |
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 | |
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 | |
10.4,8,460,215,3,5.424,17.82,0,0,3,4 | |
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 | |
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 | |
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 | |
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 | |
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 | |
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 | |
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 | |
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 | |
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 | |
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 | |
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 | |
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 | |
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 | |
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 | |
15,8,301,335,3.54,3.57,14.6,0,1,5,8 | |
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 | |
# Pandas needs Jinja2 for table styling, but it doesn't (yet) load automatically | |
# in Pyodide, so we need to explicitly list it here. | |
Jinja2 | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), | |
ui.input_action_button("go", "Go!", class_="btn-success"), | |
ui.output_plot("plot"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
# Use reactive.event() to invalidate the plot only when the button is pressed | |
# (not when the slider is changed) | |
@reactive.event(input.go, ignore_none=False) | |
def plot(): | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(input.n()) | |
fig, ax = plt.subplots() | |
ax.hist(x, bins=30, density=True) | |
return fig | |
app = App(app_ui, server) | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), | |
ui.input_action_link("go", "Go!"), | |
ui.output_plot("plot"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
# reactive.event() to invalidate the plot when the button is pressed but not when | |
# the slider is changed | |
@reactive.event(input.go, ignore_none=False) | |
def plot(): | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(input.n()) | |
fig, ax = plt.subplots() | |
ax.hist(x, bins=30, density=True) | |
return fig | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_checkbox_group( | |
"colors", | |
"Choose color(s):", | |
{ | |
"red": ui.span("Red", style="color: #FF0000;"), | |
"green": ui.span("Green", style="color: #00AA00;"), | |
"blue": ui.span("Blue", style="color: #0000AA;"), | |
}, | |
), | |
ui.output_ui("val"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def val(): | |
req(input.colors()) | |
return "You chose " + ", ".join(input.colors()) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_checkbox("somevalue", "Some value", False), | |
ui.output_ui("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def value(): | |
return input.somevalue() | |
app = App(app_ui, server) | |
from datetime import date | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_date_range( | |
"daterange1", "Date range:", start="2001-01-01", end="2010-12-31" | |
), | |
# Default start and end is the current date in the client's time zone | |
ui.input_date_range("daterange2", "Date range:"), | |
# start and end are always specified in yyyy-mm-dd, even if the display | |
# format is different | |
ui.input_date_range( | |
"daterange3", | |
"Date range:", | |
start="2001-01-01", | |
end="2010-12-31", | |
min="2001-01-01", | |
max="2012-12-21", | |
format="mm/dd/yy", | |
separator=" - ", | |
), | |
# Pass in Date objects | |
ui.input_date_range( | |
"daterange4", "Date range:", start=date(2001, 1, 1), end=date(2010, 12, 31) | |
), | |
# Use different language and different first day of week | |
ui.input_date_range("daterange5", "Date range:", language="de", weekstart=1), | |
# Start with decade view instead of default month view | |
ui.input_date_range("daterange6", "Date range:", startview="decade"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app = App(app_ui, server) | |
from datetime import date | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_date("date1", "Date:", value="2016-02-29"), | |
# Default value is the date in client's time zone | |
ui.input_date("date2", "Date:"), | |
# value is always yyyy-mm-dd, even if the display format is different | |
ui.input_date("date3", "Date:", value="2016-02-29", format="mm/dd/yy"), | |
# Pass in a Date object | |
ui.input_date("date4", "Date:", value=date(2016, 2, 29)), | |
# Use different language and different first day of week | |
ui.input_date("date5", "Date:", language="ru", weekstart=1), | |
# Start with decade view instead of default month view | |
ui.input_date("date6", "Date:", startview="decade"), | |
# Disable Mondays and Tuesdays. | |
ui.input_date("date7", "Date:", daysofweekdisabled=[1, 2]), | |
# Disable specific dates. | |
ui.input_date( | |
"date8", | |
"Date:", | |
value="2016-02-29", | |
datesdisabled=["2016-03-01", "2016-03-02"], | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app = App(app_ui, server) | |
import pandas as pd | |
from shiny import * | |
from shiny.types import FileInfo | |
app_ui = ui.page_fluid( | |
ui.layout_sidebar( | |
ui.panel_sidebar( | |
ui.input_file("file1", "Choose CSV File", accept=[".csv"], multiple=False), | |
ui.input_checkbox("header", "Header", True), | |
), | |
ui.panel_main(ui.output_ui("contents")), | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def contents(): | |
if input.file1() is None: | |
return "Please upload a csv file" | |
f: list[FileInfo] = input.file1() | |
df = pd.read_csv(f[0]["datapath"], header=0 if input.header() else None) | |
return ui.HTML(df.to_html(classes="table table-striped")) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_numeric("obs", "Observations:", 10, min=1, max=100), | |
ui.output_text_verbatim("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def value(): | |
return input.obs() | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_password("password", "Password:"), | |
ui.input_action_button("go", "Go"), | |
ui.output_text_verbatim("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
@reactive.event(input.go) | |
def value(): | |
return input.password() | |
app = App(app_ui, server) | |
from htmltools import HTML | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_radio_buttons( | |
"rb", | |
"Choose one:", | |
{ | |
"html": HTML("<span style='color:red;'>Red Text</span>"), | |
"text": "Normal text", | |
}, | |
), | |
ui.output_ui("val"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def val(): | |
return "You chose " + input.rb() | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_select( | |
"state", | |
"Choose a state:", | |
{ | |
"East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}, | |
"West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"}, | |
"Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"}, | |
}, | |
), | |
ui.output_text("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def value(): | |
return "You choose: " + str(input.state()) | |
app = App(app_ui, server) | |
ß | |
ddúd | |
dddúdúd¨¶ | |
ddúd | |
dddúdúdd | |
defdÑZ | |
ee¶ | |
New Jersey⁄Connecticut)⁄NY⁄NJ⁄CT⁄ | |
Washington⁄Oregon⁄ | |
California)⁄WA⁄OR⁄CA⁄ Minnesota⁄ Wisconsin⁄Iowa)⁄MN⁄WI⁄IA)z | |
East Coastz | |
West Coast⁄MidwestT)⁄multiple⁄valuezSelectize OptionszCustomize with JavaScriptzb{option: function(item, escape) {return "<div><strong>Select " + item.label + "</strong></div>";}})⁄placeholder⁄render)r | |
page_fluid⁄input_selectize⁄output_text⁄br⁄app_ui⁄Inputs⁄Outputs⁄Sessionr% | |
| |
| |
| |
| |
| |
| |
Ù | |
ÄD3ê& | |
Äcà&ê&—‘ÄÄÄr# | |
from htmltools import HTML | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_selectize( | |
"state", | |
"Choose a state:", | |
{ | |
"East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}, | |
"West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"}, | |
"Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"}, | |
}, | |
multiple=True, | |
), | |
ui.output_text("value"), | |
ui.br(), | |
ui.input_selectize( | |
"state", | |
"Selectize Options", | |
{ | |
"East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}, | |
"West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"}, | |
"Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"}, | |
}, | |
multiple=True, | |
options=( | |
{ | |
"placeholder": "Customize with JavaScript", | |
"render": HTML( | |
'{option: function(item, escape) {return "<div><strong>Select " + item.label + "</strong></div>";}}' | |
), | |
} | |
), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def value(): | |
return "You choose: " + str(input.state()) | |
app = App(app_ui, server) | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("obs", "Number of bins:", min=10, max=100, value=30), | |
ui.output_plot("distPlot"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot | |
def distPlot(): | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(437) | |
fig, ax = plt.subplots() | |
ax.hist(x, input.obs(), density=True) | |
return fig | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_switch("somevalue", "Some value", False), | |
ui.output_ui("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def value(): | |
return input.somevalue() | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_text_area("caption", "Caption:", "Data summary"), | |
ui.output_text_verbatim("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def value(): | |
return input.caption() | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_text("caption", "Caption:", "Data summary"), | |
ui.output_text_verbatim("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def value(): | |
return input.caption() | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("add", "Add UI"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.add) | |
def _(): | |
ui.insert_ui( | |
ui.input_text("txt" + str(input.add()), "Enter some text"), | |
selector="#add", | |
where="afterEnd", | |
) | |
app = App(app_ui, server) | |
import random | |
from shiny import * | |
app_ui = ui.page_fluid(ui.output_ui("value")) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
reactive.invalidate_later(0.5) | |
print("Random int: ", random.randint(0, 10000)) | |
@output | |
@render.ui | |
def value(): | |
reactive.invalidate_later(0.5) | |
return "Random int: " + str(random.randint(0, 10000)) | |
app = App(app_ui, server) | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("n", "Number of observations", min=0, max=1000, value=500), | |
ui.input_action_button("go", "Go!", class_="btn-success"), | |
ui.output_plot("plot"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
def plot(): | |
# Take a reactive dependency on the action button... | |
input.go() | |
# ...but don't take a reactive dependency on the slider | |
with reactive.isolate(): | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(input.n()) | |
fig, ax = plt.subplots() | |
ax.hist(x, bins=30, density=True) | |
return fig | |
app = App(app_ui, server) | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.layout_sidebar( | |
ui.panel_sidebar(ui.input_slider("n", "N", min=0, max=100, value=20)), | |
ui.panel_main(ui.output_plot("plot")), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
def plot() -> object: | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(437) | |
fig, ax = plt.subplots() | |
ax.hist(x, input.n(), density=True) | |
return fig | |
app = App(app_ui, server) | |
from shiny import * | |
ui_app = ui.page_fluid( | |
ui.markdown( | |
""" | |
# Hello World | |
This is **markdown** and here is some `code`: | |
```python | |
print('Hello world!') | |
``` | |
""" | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app = App(ui_app, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("show", "Show modal dialog"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.show) | |
def _(): | |
m = ui.modal( | |
"This is a somewhat important message.", | |
title="Somewhat important message", | |
easy_close=True, | |
footer=None, | |
) | |
ui.modal_show(m) | |
app = App(app_ui, server) | |
from shiny import * | |
# ============================================================ | |
# Counter module | |
# ============================================================ | |
@module.ui | |
def counter_ui(label: str = "Increment counter") -> ui.TagChild: | |
return ui.div( | |
{"style": "border: 1px solid #ccc; border-radius: 5px; margin: 5px 0;"}, | |
ui.h2("This is " + label), | |
ui.input_action_button(id="button", label=label), | |
ui.output_text_verbatim(id="out"), | |
) | |
@module.server | |
def counter_server( | |
input: Inputs, output: Outputs, session: Session, starting_value: int = 0 | |
): | |
count: reactive.Value[int] = reactive.Value(starting_value) | |
@reactive.Effect | |
@reactive.event(input.button) | |
def _(): | |
count.set(count() + 1) | |
@output | |
@render.text | |
def out() -> str: | |
return f"Click count is {count()}" | |
# ============================================================================= | |
# App that uses module | |
# ============================================================================= | |
app_ui = ui.page_fluid( | |
counter_ui("counter1", "Counter 1"), | |
counter_ui("counter2", "Counter 2"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
counter_server("counter1") | |
counter_server("counter2") | |
app = App(app_ui, server) | |
from typing import List | |
from shiny import * | |
from shiny.types import NavSetArg | |
from shiny.ui import h4 | |
def nav_controls(prefix: str) -> List[NavSetArg]: | |
return [ | |
ui.nav("a", prefix + ": tab a content"), | |
ui.nav("b", prefix + ": tab b content"), | |
ui.nav_control( | |
ui.a( | |
"Shiny", | |
href="https://github.com/rstudio/shiny", | |
target="_blank", | |
) | |
), | |
ui.nav_spacer(), | |
ui.nav_menu( | |
"Other links", | |
ui.nav("c", prefix + ": tab c content"), | |
"----", | |
"Plain text", | |
"----", | |
ui.nav_control( | |
ui.a( | |
"RStudio", | |
href="https://rstudio.com", | |
target="_blank", | |
) | |
), | |
align="right", | |
), | |
] | |
app_ui = ui.page_navbar( | |
*nav_controls("page_navbar"), | |
title="page_navbar()", | |
bg="#0062cc", | |
inverse=True, | |
id="navbar_id", | |
footer=ui.div( | |
{"style": "width:80%;margin: 0 auto"}, | |
ui.tags.style( | |
""" | |
h4 { | |
margin-top: 3em; | |
} | |
""" | |
), | |
h4("navset_tab()"), | |
ui.navset_tab(*nav_controls("navset_tab()")), | |
h4("navset_pill()"), | |
ui.navset_pill(*nav_controls("navset_pill()")), | |
h4("navset_tab_card()"), | |
ui.navset_tab_card(*nav_controls("navset_tab_card()")), | |
h4("navset_pill_card()"), | |
ui.navset_pill_card(*nav_controls("navset_pill_card()")), | |
h4("navset_pill_list()"), | |
ui.navset_pill_list(*nav_controls("navset_pill_list()")), | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
print("Current navbar page: ", input.navbar_id()) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.layout_sidebar( | |
ui.panel_sidebar( | |
ui.input_radio_buttons( | |
"controller", "Controller", ["1", "2", "3"], selected="1" | |
) | |
), | |
ui.panel_main( | |
ui.navset_hidden( | |
ui.nav(None, "Panel 1 content", value="panel1"), | |
ui.nav(None, "Panel 2 content", value="panel2"), | |
ui.nav(None, "Panel 3 content", value="panel3"), | |
id="hidden_tabs", | |
) | |
), | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.controller) | |
def _(): | |
ui.update_navs("hidden_tabs", selected="panel" + str(input.controller())) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("show", "Show"), | |
ui.input_action_button("remove", "Remove"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
ids: list[str] = [] | |
n: int = 0 | |
@reactive.Effect | |
@reactive.event(input.show) | |
def _(): | |
nonlocal ids | |
nonlocal n | |
# Save the ID for removal later | |
id = ui.notification_show("Message " + str(n), duration=None) | |
ids.append(id) | |
n += 1 | |
@reactive.Effect | |
@reactive.event(input.remove) | |
def _(): | |
nonlocal ids | |
if ids: | |
ui.notification_remove(ids.pop()) | |
app = App(app_ui, server, debug=True) | |
from datetime import datetime | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("close", "Close the session"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
def log(): | |
print("Session ended at: " + datetime.now().strftime("%H:%M:%S")) | |
session.on_ended(log) | |
@reactive.Effect | |
@reactive.event(input.close) | |
async def _(): | |
await session.close() | |
app = App(app_ui, server) | |
from datetime import datetime | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("flush", "Trigger flush"), | |
ui.output_ui("n_clicks"), | |
ui.div(id="flush_time"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
def log(): | |
msg = "A reactive flush occurred at " + datetime.now().strftime("%H:%M:%S:%f") | |
print(msg) | |
ui.insert_ui( | |
ui.p(msg), | |
selector="#flush_time", | |
) | |
session.on_flush(log, once=False) | |
@output | |
@render.ui | |
def n_clicks(): | |
return "Number of clicks: " + str(input.flush()) | |
app = App(app_ui, server) | |
from datetime import datetime | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("flush", "Trigger flush"), | |
ui.output_ui("n_clicks"), | |
ui.div(id="flush_time"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
def log(): | |
msg = "A reactive flush occurred at " + datetime.now().strftime("%H:%M:%S:%f") | |
print(msg) | |
ui.insert_ui( | |
ui.p(msg), | |
selector="#flush_time", | |
) | |
session.on_flushed(log, once=False) | |
@output | |
@render.ui | |
def n_clicks(): | |
return "Number of clicks: " + str(input.flush()) | |
app = App(app_ui, server) | |
from shiny import * | |
from shiny.types import ImgData | |
app_ui = ui.page_fluid(ui.output_image("image")) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.image | |
def image(): | |
from pathlib import Path | |
dir = Path(__file__).resolve().parent | |
img: ImgData = {"src": str(dir / "rstudio-logo.png"), "width": "150px"} | |
return img | |
app = App(app_ui, server) | |
âPNG | |
IHDR | |
óY#€ª≥a¨k««Gm:1fìÒnbª#i"ê;’≈ûssãπõùœ≥SsYov!ü]Ϫc˝L˜{Ex à<-¬‡(–{5Å#Ø∆ÄÀÅ[Uπ›©^oD.µ´6ØÔ&ónÂÚÌc\ºeî-∫¨[ï2>ö–MÖƒ$‹Ö*‰NÈısã9”ß2éL-Ú‚·ˆúcÔÅ9]»NŒe”YÆ/à®πxxò•¡ëWÏ | |
·£∫ÚU°◊wLœˆy·–OÏô··g¶Ÿı‚,'Neã™∫fly | |
kRnπz | |
w‹∏ûÎ.[≈˙’+^¯™K…~˘7+`Dp™úúÕÿµoñØ<~úØ}„8é.ê;ù7"œ | |
¯a‡ | |
™¬˙U)∑Ô\«€n›ƒUè3⁄58ı£öBŒ≈UÍ 8"–œîœÛ•GéÒ∑„¿‰™®/`> | |
<Ù_`‰e | |
¯p–=ü†»y÷äÌ¿è0Æ | |
õ◊u¯Ó7mÊŒ◊odÌDäS-qêÛÜ,„}cÑ˘≈úØ?}ÇO˛›KÏŸ?˜‡C¿«Ä√ÁK[‰<Åëw?|ê7~ıéq~Ëm€π˛äU#ïÅ~9$fl¢.“8$‚èΩpxû?˝ÎÉ|Ì…dπNB¯;‡7Å˚B‹sNAëÛ | |
õ÷uŸΩñìsyî5¿Ìå« | |
cü\ÚO»_¯ÿ˘§∆*‡flˇ6º–Ω[Ø]Àõo⁄įƒ_yÉÒû´¢™˛˝WÂÓRÓîUcfi,Ózq∂M€«Ä7Æ{®–îïÇíú!L | |
kRæ˝⁄µÅ^¶KÚcaQt•ÓÎÀÏÜâ¿ŒÀWÒÿÆûyq3xç´Ç\ÆÒ¬JOóúÅvå | |
¸ÁÇä∂¬ÒQÀ’è£@?sµËK_Ó4Û9à3µÊ | |
ól„ÇuL.sVøL∏ÂÇí¨ | |
Ωâ÷uÿ∏∂C/sCSSz¶“y≈›ce¥kÿ±yîìãK}p´¿K¡%f9†$+˙~≠È⁄∂˘—[7åêZ©—’+PúÛÕà∞u„i"√h´ÿ.~__yj9†,◊Ül ¯ÎNÎ∂Ya˝Íî<˜fi”ˇèõà≤f<°€1Ù]‡Êv‡ßÄcgLYëv§!7ıée%+åçX≤‹ªçCØV©å ô0“+aá4ÿëN*åt,ßÊÚÂ|Ì{ÅGÅˇ‰KiI≤™∫3$ | |
ìÂ\¨5Ç5DˆCW&Âeņg@~“ | |
˚Å*æÇô⁄eª4‰A‡fi≥1Í€Ò)ÙÕ+πÌ‹˘J‹ÈK√∫‰/ 0…I˚9§ÜA3¡^˘yzπ§<◊ï¢yQêÂS¿‰0-Iñ–É/.}«JŒÍ‘w}x@Ù4√^1∆ß(Ú‹ÖºS≈R∞ù4\œPõ¡X„K&|(W/¨‹ÖÛãïøK@µ@≤ƒ ÈııtΩmª¯¡ü¥¯•4§(ª&+—é,Wfr˙y√Âm «UïõÆZ√ï;∆yÈËì'ô9’gæóìß¿X!µÜn«0>bôMX=û≤z<a’X¬X7°”1$∆¥ü)sS3}O-pË<ì”=zAc͇hEÄq_Oc‘Ász}∑‚T`†ˇøæyZ 䥣KTv]…ñ;e˙T∆ñ | |
√(´¢)UÂÇufir”F\ËßZË;s˙ô√©˝ùD˧ñnjH¨êX)[Çñ⁄˙ô„‰\∆ãáÁxl˜ æ±wöß˙àDL!ëFÌE8⁄àpr.c±ÁŒ$ì|-£¡Û öZ2lÙ[Vº9«fz,ˆF.3OER—ß∏ÖëÆe§ka"='Qö÷ØÓ∞~uák/]ÕfiÉ≥¸ıCáyl˜ zYé16 | |
Ìx?Ê≥Ò | |
7ØÎ`ç¶÷£>´\Wl„™´„Gfiù.4I.ik|dÑçk∫\{…j¨ÅΩN≤–œK | |
æ∏á~¶Ï90«âìŸô÷Z÷ßıygÑ€4‰˙Â∆̸ | |
'Á2&OÙÿ∂q$÷¿⁄Ü cÚp|Ì©)ücÌ∏eÌD ∫U)k&R∂mgÀ˙±ö–ù‘≥Û}NÕıô]»XËÂÙ˙9ôsFª ÷å∞}”mû`ÌD∑û˘Mx◊m€Å?øoΩL1∆g¬TqQA_N´f=ı°”Ãl∆±ÈfiŸ*ªÅ?$*ˇ∂bU]x6g reˇ‰ÎV•$÷ªKbq<$- | |
¸ÅcÏõ<Ö∫!√‡∞∆q◊∑_ƒ?}Ák±Å˜˙ô„œ˛ÊYÓ¸ | |
∫⁄q6⁄k…±Èì”=ÔÜïœT…T˝1Á]T1`Cœ¡©«ÏB^suT·‘\èìs=≤úLA≈†∆ǵàMõÄM»≈0≥êÛ‰Û«˘˝œìfl˘Ûo∞Ô»©⁄5év-w›∫çu´,Ω¨OñÁdπ#sæhñ∫∫^ˇ˜‰|Œ°„ã^€ÕY!bÇñlç10ç›\uˆ…»Ï?∫»\/Øn,ÄPÇчúúXúX0â≤1≠°ãäA≈¢∆¢&õÇÌ@vÎw±L⁄•Øñøjí?¯¸3Lù¨gg/›:¡ŒÀ◊“œ3˙yNÓ<ıeZÄ‚A»úí)Ùùrpjë˘û√OOåúMÁ:‡∂&J≈6‹å˙Yiá1ǵ¬Ã\∆KS=˙πV Ä√ôcJ@‘$^»&ıZ”D∆¢&≠Å!II∫H⁄ŧˆp,I˘á]S‹˜¯Kçºõ·uó≠≈%À32Á»’ë;–ßpÏdü…ôbcç•p¡œîq‡Ì±ÃMDWó∑ûF ãÎGœëÈSß˙• ó(9‡0a‰'8„w;pì8É3l–é$-µ¢ | |
§‚oO-ˇÙQfÊj=”\∏iå±Æ–œ32óG@T◊ÎÄì9ß<◊¿à@9C/Ò∂êV°{«jF˝÷”’:NFPac´Á | |
ßzòƒ∞j,°∞ÈDaô7Ù¶Ã˛™ÒÌÁÉî%8SñELäö‰E3w¥$ƒ°‚cå˝«9rbÅ’cUú≥fº√ÿàÂt∆y | |
øß!Ω∞ÿsºt|ë≈Ãa≠‡ƒß»ä4ôT£.g•eáãCä~OÏeuÒm,ù≥√ | |
∆‡5§ | |
1Öä!ÉM}|oÛã9≥ãYF†∫Ö^ŒÙúc1Ûö°∆è|ì·;j¿8Æ⁄ø^(óó∆Ä\C[≥€ŸÄiGe[¸(õYpddLå&§©D¥ÂˇÁÇ6– | |
àB! ö‚ | |
™‘\oÜÊr`√Í.¨≠;ëGgòûΰ9ä:eæü3◊ì`¿=öG◊ `"Rdã¶E(∫,õ≤∏x0 %⁄kVîfóXÿTFº | |
*ïñ‡øw„ekÿ∏™n&wúff>√ë–À`1wdŒk† | |
6√ÁfiVY˚ fÒÉ4@)™‘Ó¥ÂÕN¿¿$¡æl%AüèRÉMµæ∆PiF | |
∆ó!ÄQhV©R…‘EQı | |
”ú cë∞çYåJ√ÍÄH(å;⁄Sπ˙îÜ8áq„r?HBÅ+a45lúHπzÛ(oºb | |
7_<¡ö—∫yÏeé?˘ ^Óvõ&à âIo+ò]Ω5¢Ú®" ¥|ßïv N”÷∂≠.HÇv§Àµπ≥F5§§)€L”Û≤Ë`Ë+Gi∞DõX·-◊maÎÜq0∂L∑kȶñ’# &R∂¨Óp·⁄.':…`>læóÛâØ>œ«æ≤óL!I0äî | |
’òÆ¥ïK;e)`¥™«kó mCÆΩ9¢l(lH≤\™*FvSCƒT˘´V-J©% "π≈ƒfiyÛÖ|WAWZp (߀ˆOÕÒ'_ŸÀg⁄«lflëtR0)bRÔflàløT.nÂI5lFDG¶–í†)&\§3Çq¡ˆ¢AÍJĵ æq⁄û^;†4é◊Äi–T´a∑u◊π“Zk(‚éï4Pià5æ∫ÎÒ˚xzˇ4N!ÈtJ≠ Ä`ÉÌtínÉû‘0 | |
^U•8p&D÷≤4UÑX“Rѯü-ùkhä @àë÷ÿƒ‡''j+_«öπ¶uPÜ#%≈`b√eáãÇOEy≠CQ÷ ÷ âıªµÿ4·‡LèO<Ù"3Uz›àŒ.‚çWn¡$õ&~Ôt∞ùõ¶ÿ$¡$k | |
í¯‡0Œ\∑'M+˘ò»„,ãsRd$⁄î_=˝¶@fl‡Á¬π∂õØiGJÌD | |
mëJËq¨—§´8Á€ô¢⁄h¢˙F)`k∞â≈vRí4%Èv´}§√É˚¶˘ÎoÆE˘k∆:¸–móq—∆ ∞ìZˇ;â≈™ΩÈVi¢°fieÏËH€ | |
ì!bƒ«âıv ∆vRH∑Cû$|~◊1v≠7«]πiǘϋÏù˝¢VdåÔp01 | |
2 | |
Ω∏F*à JC#"≠†'√ | |
)ìxkêH´=¡‘ | |
ñ»¿H≥FRä^ñ ÉëÕ±â/µñ¿§fi-NªrõÖÁé±{j∂fiº~åÔªÓ∆PÕ´÷â†)&¶g3Ë‚W-§m1G€Änë€ ÎÏ…˙˘ú ÷”@&Qt>E”†´!'(ÁyHKp9,ÜiÌ∞.¿2+çø)]XvìJöêvS&üyvíìΩ:uΩÈ“ıº)PWQÙ.Ï Ü·n~”ëiqi+πP | |
Lóh≈_ä¢Btp$¥çí `µ™ˆç©Úa√lŧ-¡1%uy◊ÿ˙«éŒrfl«k‘5öZæÔ∫Õ\∂Æãs¡ûÖ€6äé5∑6Ñ“b/–§∫º | |
s~J@[áÜN}Àé‚p8flW∫≠ò$PX”Wá«éŒÒÂΑ’MflsÌfÆX?Bøfl'À˚‰yFûÁ8¿ë(^â˙bñhjM óFé∞x | |
s"|5Èÿ§*›ä†àÏmçµ8i”0≈#!:)C4¶Ú Ö´;ÏXõF@*™9óÆÌ÷hÀºf√NÑ¥€Ò˘¨4≈$ √¡E«ÒÃ!‚º«$÷w√´“œsÓy˛8ØY?Œï—¸≈ÌkFxÔ | |
[˘´]G…ãBÁ`◊‰GOfıÖæZYB#Ü–»Ê∆2âV®d˚<¶öçõ,fi˜v∆fi˙E}^ÑHL‹J]c§iZTëÿ†È•˘¬ˇ.„Ω;◊—\*ÃöjÄ‘fi}’VÚ⁄z6˛7sU>±Ô$ú»Bå¶8ÅT-÷)⁄I92ø»ÁvOÚœo‹Œ™NUz˝Ekπ~€Í⁄≠/féflº˜ | |
4^íFı | |
•Zºëlré˚‘%B̸≈np8ó°.Ø‹„∏ÛXÍ≈<hD©/ËQ¥°Ü'E∏«¶¶Wü“VNáê/„üü—¸"uZî˙(ê˙ç—∞3⁄ | |
åm^.bï∆sLjˆÑAõ;P“@¢∑ûëj`Óofl∆™ª˛ä<s˚≠µw7ˆ0√Äj”ÍÙŸLìUÑ/úX¨FEº§ì‘ócäÌT ;`.À*0 Ühâáµì$ÿ‘qdaÅøÿ5…{ÆÇé?ÂôzÌ¥ó)≥=p5Q˜ï‘›V®ö3¥eêï}«ıʸ\E>˚∆ˇ¯ÊCˇK_‚ƒˇ}k}ûz÷áN◊:Öœà√ | |
ó»2∆Ø6¿àÑ’∆—˙ïÚ∑¶y¯¿tàòsѨt;KÔ¶÷—Råvk¯<V¬ÇÒÀ∆îZ@1àU_ÚM\íÚËë9û=Ú<Ù˚‰˝å<À—‹Ø’´ÍıwzA1bVËjQÄ∆?bõÿ“Æ(ÏA∏˚kø|oç:K@í¥‡}ˇdôü*[—d ™rQmØ•6O ˇoÆØÃıÛ | |
MQ0òÄqæ—¿™‡[€I◊í | |
”5ÉUœ0cpAS≤‹qb°G∂®‰=Gfiw∏<«ÂäsæEƒ˙åÇ™–⁄ *Kw}å^fl˘iî]>”A@NfiÛèπ‡˚æD/s=îè | |
Ú›≈ÏPñXYTܸ{ÿÎ∂ÆOÿ$Zü}-ÎT≠©µŸØ!wU§Jí±ñZ¯ØÒ–—≤¶‡ßO$òD±i° ëú<wHÊ|ß°zP|WJµzP’ø#úv-߈5 ˜ ÚßI*y?œ8ˆÈ∑–ÎÂàTıD>Ö_πz…≥Iù´ZáFØ¥·/RˆWIflMÜç2ºV0IàƒMQåıç | |
uÆW≠ÅUÀ©∆⁄)1±äI<=Y5 9d1Œ˜fl:Ô:;ï–¨ª!¬Æ∞ÍD¯∏S}Jå`M£µ2–âªøìÖk˙¿áÔqiq2 | |
o‘k€îÀ,UOz´|oø$ᱩ?os∑)í¯>. ö'ÕÓtPÕµ&5˙ê èÉ|4ILéÉ#ü|Ûp@ | |
ãÊ≈(≈.•FsX$Z<¿îTBBÒ∫ö¢Fi=᧴æ\?Oºö/Æ·I≈j>^kjEã“∂‘œflÏØGN ’˙Ä-µ2∫˜fi_üö=Í(zË„wúê“o6`≠9å_¡@|“Å’¥A£jÄR“HÙï˙≠¶%\ûܸÂö∫° | |
<Ø.ƒp>>Ù+*@‰(⁄\XôÍ|q~$Zü±∫Ó˙}◊=mXˇw/¬o+S$pcÌkã∂rÙ”ˇ®¥≈™r˛XΩÅïV£—Q£Ñ⁄⁄Ö1Xa˘m±CZ.€ eÉrLCÂ_◊ | |
?/˛ÊäÀΩßTÏû£—ÓjØ– ık´Ctü4Ï|·èl'q | |
Jæ | |
|–ôŒN≤ˇ#o[R‰f©7ˇŸaæ7òƒ|ˇ¨©®(“éøj]#ä◊ıuãõ7fiπ±(5¢˘.˙ÎZ4√’©´∞+y˝˜5z¥ë´iOhµÃ_Ü∆‘)ö= | |
+ªE¿vOøX∆iüAuÍf’Œ6ÕÌ5∆L#rªàåôf±_™˘~CªeX˚å‘k | |
çB›Àk⁄õ"3Æ%ΩU•&ÙÚu^7¯}fi)ıcy|^ˇ\aü‚sîÔ©RÂNˆÓN;^Ãfl˙–mg¿…'>ÃöfiᣆOäH_åºAD∫q˚ãËo¨A©◊I⁄ä=Q™=WTs"{®-VY *BcA÷ | |
~© | |
BnkSÇ]˛˚8 خڗtƒ€çÁÔ∂eÖÒÀ~J€Ã„fÕçÔ√„Ä«ƒ◊,oëN•2– | |
‘÷…G§≈“±≠Z1ê◊‘v | |
qM˙k∆!±-©ÑZë∑ | |
î„¡˘˘_∆HOvˇèï≠ö∏¢ÁŒ<Òá¨π˘}E´exëYπEѱ°≠˘«Z⁄} ñ ©≥î–û,ä | |
G›;rÆç∂Z"¯ÿ¶¥kãV∂§ ä?~ÂW‘ÈÔâHÅ›ˇ}ÂKXƯIü”è¸În˘Ò¢1CÂ!Å‹(¬⁄∂fi•ˆΩLö‘#juPjë>¥D«•^QlOåª÷å¥ãº™˙kNCe±=™˙(%–‘≥ø}fÎâ⁄3˘“âáüµ∑¸DË¡≈°Ó 1ÊiÑ´Eÿ6T#h4”n7ZS¯⁄–é0%∂û’≠Ç…ÿ†∑iIƒ˘A3¿9WéxkE‰rR_E˘ô≈Eπ€ZúàÃ}˝YÙúÂv≈ø| ÃˇÄk≈»åëÔ5V∫’"∆O!hNÏl]ÖÍfl“ò"÷“∏<P¿ßX‹T#Î8˛qÿé | |
òà∫Ú˙Î(æô◊\?°éªãßH?˝õ∑úe√∆9ÿ^ÛØæ¶ | |
È©ˇ|ÀYÀÚú=ËÏ ü~êÖE«ÿàÒkXfid帨π”Ø-Ò*A√ó⁄†1-!^¢1w•—Ç4D—ˢÉÄ | |
™äõπâ[U©W1â≤œ⁄Úÿ ä»ΩÉTF]ù”Á‘Òqu˙µ∫õæ_w≈·Òfl∏ÒúÀÓº>õÒ∫üEã›D¨\/Ffi+¬ª≈»e∆ä1çilÕuS©*2Ï»@s^≥|LãqWWî¥ë∑*mIÆN˜®”O´Úß.◊ßÚ‚w˝7ú7ôù˜áeæÓ˝˚^(ÁäyÉâ1ÚZ1ÚÆ | |
ï≠Õ≠`X®Æµf»(k | |
Ôã™:L¢ÏSt ”™<ÉÚ-uz\åߧnjD‡¡_|›+&ìWêxªÂ◊ü | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider( | |
"n", "input_slider()", min=10, max=100, value=50, step=5, animate=True | |
), | |
ui.output_plot("p"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot | |
def p(): | |
np.random.seed(19680801) | |
x_rand = 100 + 15 * np.random.randn(437) | |
fig, ax = plt.subplots() | |
ax.hist(x_rand, int(input.n()), density=True) | |
return fig | |
app = App(app_ui, server) | |
import pathlib | |
import pandas as pd | |
from shiny import * | |
dir = pathlib.Path(__file__).parent | |
mtcars = pd.read_csv(dir / "mtcars.csv") | |
app_ui = ui.page_fluid( | |
ui.input_checkbox("highlight", "Highlight min/max values"), | |
ui.output_table("result"), | |
# Legend | |
ui.panel_conditional( | |
"input.highlight", | |
ui.panel_absolute( | |
"Yellow is maximum, grey is minimum", | |
bottom="6px", | |
right="6px", | |
class_="p-1 bg-light border", | |
), | |
), | |
class_="p-3", | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.table | |
def result(): | |
if not input.highlight(): | |
# If we're not highlighting values, we can simply | |
# return the pandas data frame as-is; @render.table | |
# will call .to_html() on it. | |
return mtcars | |
else: | |
# We need to use the pandas Styler API. The default | |
# formatting options for Styler are not the same as | |
# DataFrame.to_html(), so we set a few options to | |
# make them match. | |
return ( | |
mtcars.style.set_table_attributes( | |
'class="dataframe shiny-table table w-auto"' | |
) | |
.hide(axis="index") | |
.format( | |
{ | |
"mpg": "{0:0.1f}", | |
"disp": "{0:0.1f}", | |
"drat": "{0:0.2f}", | |
"wt": "{0:0.3f}", | |
"qsec": "{0:0.2f}", | |
} | |
) | |
.set_table_styles( | |
[dict(selector="th", props=[("text-align", "right")])] | |
) | |
.highlight_min(color="silver") | |
.highlight_max(color="yellow") | |
) | |
app = App(app_ui, server) | |
mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb | |
21,6,160,110,3.9,2.62,16.46,0,1,4,4 | |
21,6,160,110,3.9,2.875,17.02,0,1,4,4 | |
22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 | |
21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 | |
18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 | |
18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 | |
14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 | |
24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 | |
22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 | |
19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 | |
17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 | |
16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 | |
17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 | |
15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 | |
10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 | |
10.4,8,460,215,3,5.424,17.82,0,0,3,4 | |
14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 | |
32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 | |
30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 | |
33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 | |
21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 | |
15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 | |
15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 | |
13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 | |
19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 | |
27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 | |
26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 | |
30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 | |
15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 | |
19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 | |
15,8,301,335,3.54,3.57,14.6,0,1,5,8 | |
21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 | |
# Pandas needs Jinja2 for table styling, but it doesn't (yet) load automatically | |
# in Pyodide, so we need to explicitly list it here. | |
Jinja2 | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_text("txt", "Enter the text to display below:"), | |
ui.row( | |
ui.column(6, ui.output_text("text")), | |
ui.column(6, ui.output_text_verbatim("verb", placeholder=True)), | |
), | |
ui.row( | |
ui.column(6), | |
ui.column(6, ui.output_text_verbatim("verb_no_placeholder", placeholder=False)), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
def text(): | |
return input.txt() | |
@output | |
@render.text | |
def verb(): | |
return input.txt() | |
@output | |
@render.text | |
def verb_no_placeholder(): | |
return input.txt() | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("add", "Add more controls"), | |
ui.output_ui("moreControls"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
@reactive.event(input.add) | |
def moreControls(): | |
return ui.TagList( | |
ui.input_slider("n", "N", min=1, max=1000, value=500), | |
ui.input_text("label", "Label"), | |
) | |
app = App(app_ui, server) | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fixed( | |
ui.layout_sidebar( | |
ui.panel_sidebar(ui.input_slider("n", "N", min=0, max=100, value=20)), | |
ui.panel_main(ui.output_plot("plot")), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
def plot() -> object: | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(437) | |
fig, ax = plt.subplots() | |
ax.hist(x, input.n(), density=True) | |
return fig | |
app = App(app_ui, server) | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.layout_sidebar( | |
ui.panel_sidebar(ui.input_slider("n", "N", min=0, max=100, value=20)), | |
ui.panel_main(ui.output_plot("plot")), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
def plot() -> object: | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(437) | |
fig, ax = plt.subplots() | |
ax.hist(x, input.n(), density=True) | |
return fig | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.panel_title("A basic absolute panel example", "Demo"), | |
ui.panel_absolute( | |
ui.panel_well( | |
"Drag me around!", ui.input_slider("n", "N", min=0, max=100, value=20) | |
), | |
draggable=True, | |
width="300px", | |
right="50px", | |
top="50%", | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_checkbox("show", "Show radio buttons", False), | |
ui.panel_conditional( | |
"input.show", ui.input_radio_buttons("radio", "Choose ", ["slider", "select"]) | |
), | |
ui.panel_conditional( | |
"input.show && input.radio === 'slider'", | |
ui.input_slider("slider", None, min=0, max=100, value=50), | |
), | |
ui.panel_conditional( | |
"input.show && input.radio === 'select'", | |
ui.input_select("slider", None, ["A", "B", "C"]), | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid(ui.panel_title("Page title", "Window title")) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app = App(app_ui, server) | |
import asyncio | |
import random | |
import sqlite3 | |
from datetime import datetime | |
from typing import Any, Awaitable | |
import pandas as pd | |
from shiny import * | |
SYMBOLS = ["AAA", "BBB", "CCC", "DDD", "EEE", "FFF"] | |
def timestamp() -> str: | |
return datetime.now().strftime("%x %X") | |
def rand_price() -> float: | |
return round(random.random() * 250, 2) | |
# === Initialize the database ========================================= | |
def init_db(con: sqlite3.Connection) -> None: | |
cur = con.cursor() | |
try: | |
cur.executescript( | |
""" | |
CREATE TABLE stock_quotes (timestamp text, symbol text, price real); | |
CREATE INDEX idx_timestamp ON stock_quotes (timestamp); | |
""" | |
) | |
cur.executemany( | |
"INSERT INTO stock_quotes (timestamp, symbol, price) VALUES (?, ?, ?)", | |
[(timestamp(), symbol, rand_price()) for symbol in SYMBOLS], | |
) | |
con.commit() | |
finally: | |
cur.close() | |
conn = sqlite3.connect(":memory:") | |
init_db(conn) | |
# === Randomly update the database with an asyncio.task ============== | |
def update_db(con: sqlite3.Connection) -> None: | |
"""Update a single stock price entry at random""" | |
cur = con.cursor() | |
try: | |
sym = SYMBOLS[random.randint(0, len(SYMBOLS) - 1)] | |
print(f"Updating {sym}") | |
cur.execute( | |
"UPDATE stock_quotes SET timestamp = ?, price = ? WHERE symbol = ?", | |
(timestamp(), rand_price(), sym), | |
) | |
con.commit() | |
finally: | |
cur.close() | |
async def update_db_task(con: sqlite3.Connection) -> Awaitable[None]: | |
"""Task that alternates between sleeping and updating prices""" | |
while True: | |
await asyncio.sleep(random.random() * 1.5) | |
update_db(con) | |
asyncio.create_task(update_db_task(conn)) | |
# === Create the reactive.poll object =============================== | |
def tbl_last_modified() -> Any: | |
df = pd.read_sql_query("SELECT MAX(timestamp) AS timestamp FROM stock_quotes", conn) | |
return df["timestamp"].to_list() | |
@reactive.poll(tbl_last_modified, 0.5) | |
def stock_quotes() -> pd.DataFrame: | |
return pd.read_sql_query("SELECT timestamp, symbol, price FROM stock_quotes", conn) | |
# === Define the Shiny UI and server =============================== | |
app_ui = ui.page_fluid( | |
ui.row( | |
ui.column( | |
8, | |
ui.markdown( | |
""" | |
# `shiny.reactive.poll` demo | |
This example app shows how to stream results from a database (in this | |
case, an in-memory sqlite3) with the help of `shiny.reactive.poll`. | |
""" | |
), | |
class_="mb-3", | |
), | |
), | |
ui.input_selectize("symbols", "Filter by symbol", [""] + SYMBOLS, multiple=True), | |
ui.output_ui("table"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session) -> None: | |
def filtered_quotes(): | |
df = stock_quotes() | |
if input.symbols(): | |
df = df[df["symbol"].isin(input.symbols())] | |
return df | |
@output | |
@render.ui | |
def table(): | |
return ui.HTML( | |
filtered_quotes().to_html( | |
index=False, classes="table font-monospace w-auto" | |
) | |
) | |
app = App(app_ui, server) | |
import asyncio | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("button", "Compute"), | |
ui.output_text("compute"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.text | |
@reactive.event(input.button) | |
async def compute(): | |
with ui.Progress(min=1, max=15) as p: | |
p.set(message="Calculation in progress", detail="This may take a while...") | |
for i in range(1, 15): | |
p.set(i, message="Computing") | |
await asyncio.sleep(0.1) | |
# Normally use time.sleep() instead, but it doesn't yet work in Pyodide. | |
# https://github.com/pyodide/pyodide/issues/2354 | |
return "Done computing!" | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("rmv", "Remove UI"), | |
ui.input_text("txt", "Click button above to remove me"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.rmv) | |
def _(): | |
ui.remove_ui(selector="div:has(> #txt)") | |
app = App(app_ui, server) | |
from shiny import * | |
from shiny.types import ImgData | |
app_ui = ui.page_fluid(ui.output_image("image")) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.image | |
def image(): | |
from pathlib import Path | |
dir = Path(__file__).resolve().parent | |
img: ImgData = {"src": str(dir / "rstudio-logo.png"), "width": "150px"} | |
return img | |
app = App(app_ui, server) | |
from shiny import * | |
from shiny.types import SafeException | |
app_ui = ui.page_fluid( | |
ui.input_action_button("safe", "Throw a safe error"), | |
ui.output_ui("safe"), | |
ui.input_action_button("unsafe", "Throw an unsafe error"), | |
ui.output_ui("unsafe"), | |
ui.input_text( | |
"txt", | |
"Enter some text below, then remove it. Notice how the text is never fully removed.", | |
), | |
ui.output_ui("txt_out"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Calc | |
def safe_click(): | |
req(input.safe()) | |
return input.safe() | |
@output | |
@render.ui | |
def safe(): | |
raise SafeException(f"You've clicked {str(safe_click())} times") | |
@output | |
@render.ui | |
def unsafe(): | |
req(input.unsafe()) | |
raise Exception(f"Super secret number of clicks: {str(input.unsafe())}") | |
@reactive.Effect | |
def _(): | |
req(input.unsafe()) | |
print("unsafe clicks:", input.unsafe()) | |
# raise Exception("Observer exception: this should cause a crash") | |
@output | |
@render.ui | |
def txt_out(): | |
req(input.txt(), cancel_output=True) | |
return input.txt() | |
app = App(app_ui, server) | |
app.sanitize_errors = True | |
import matplotlib.pyplot as plt | |
import numpy as np | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.row( | |
ui.column(4, ui.input_slider("n", "N", min=0, max=100, value=20)), | |
ui.column(8, ui.output_plot("plot")), | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.plot(alt="A histogram") | |
def plot() -> object: | |
np.random.seed(19680801) | |
x = 100 + 15 * np.random.randn(437) | |
fig, ax = plt.subplots() | |
ax.hist(x, input.n(), density=True) | |
return fig | |
app = App(app_ui, server) | |
from shiny import * | |
from shiny.types import SafeException | |
app_ui = ui.page_fluid(ui.output_ui("safe"), ui.output_ui("unsafe")) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def safe(): | |
raise SafeException("This is a safe exception") | |
@output | |
@render.ui | |
def unsafe(): | |
raise Exception("This is an unsafe exception") | |
app = App(app_ui, server) | |
app.sanitize_errors = True | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_text("msg", "Enter a message"), | |
ui.input_action_button("submit", "Submit the message"), | |
# It'd be better to use ui.insert_ui() in order to implement this kind of | |
# functionality...this is just a basic demo of how custom message handling works. | |
ui.tags.div(id="messages"), | |
ui.tags.script( | |
""" | |
$(function() { | |
Shiny.addCustomMessageHandler("append_msg", function(message) { | |
$("<p>").text(message.msg).appendTo("#messages"); | |
}); | |
}); | |
""" | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
@reactive.event(input.submit) | |
async def _(): | |
await session.send_custom_message("append_msg", {"msg": input.msg()}) | |
app = App(app_ui, server, debug=True) | |
from shiny import * | |
from shiny.types import SilentCancelOutputException | |
app_ui = ui.page_fluid( | |
ui.input_text( | |
"txt", | |
"Delete the input text completely: it won't get removed below the input", | |
"Some text", | |
width="400px", | |
), | |
ui.output_ui("txt_out"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def txt_out(): | |
if not input.txt(): | |
raise SilentCancelOutputException() | |
return "Your input: " + input.txt() | |
app = App(app_ui, server) | |
from shiny import * | |
from shiny.types import SilentException | |
app_ui = ui.page_fluid( | |
ui.input_text( | |
"txt", | |
"Enter text to see it displayed below the input", | |
width="400px", | |
), | |
ui.output_ui("txt_out"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@output | |
@render.ui | |
def txt_out(): | |
if not input.txt(): | |
raise SilentException() | |
return "Your input: " + input.txt() | |
app = App(app_ui, server) | |
from shiny import App, render, ui | |
app_ui = ui.page_fluid( | |
ui.h2("Hello Shiny!"), | |
ui.input_slider("n", "N", 0, 100, 20), | |
ui.output_text_verbatim("txt"), | |
) | |
def server(input, output, session): | |
@output | |
@render.text | |
def txt(): | |
return f"n*2 is {input.n() * 2}" | |
app = App(app_ui, server) | |
from htmltools import br | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("update", "Update other buttons and link"), | |
br(), | |
ui.input_action_button("goButton", "Go"), | |
br(), | |
ui.input_action_button("goButton2", "Go 2", icon="ü§©"), | |
br(), | |
ui.input_action_button("goButton3", "Go 3"), | |
br(), | |
ui.input_action_link("goLink", "Go Link"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
req(input.update()) | |
# Updates goButton's label and icon | |
ui.update_action_button("goButton", label="New label", icon="üìÖ") | |
# Leaves goButton2's label unchanged and removes its icon | |
ui.update_action_button("goButton2", icon=[]) | |
# Leaves goButton3's icon, if it exists, unchanged and changes its label | |
ui.update_action_button("goButton3", label="New label 3") | |
# Updates goLink's label and icon | |
ui.update_action_link("goLink", label="New link label", icon="üîó") | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.tags.p("The first checkbox group controls the second"), | |
ui.input_checkbox_group( | |
"inCheckboxGroup", "Input checkbox", ["Item A", "Item B", "Item C"] | |
), | |
ui.input_checkbox_group( | |
"inCheckboxGroup2", "Input checkbox 2", ["Item A", "Item B", "Item C"] | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
x = input.inCheckboxGroup() | |
if x is None: | |
x = [] | |
elif isinstance(x, str): | |
x = [x] | |
# Can also set the label and select items | |
ui.update_checkbox_group( | |
"inCheckboxGroup2", | |
label="Checkboxgroup label " + str(len(x)), | |
choices=x, | |
selected=x, | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("controller", "Controller", min=0, max=1, value=0, step=1), | |
ui.input_checkbox("inCheckbox", "Input checkbox"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
# True if controller is odd, False if even. | |
x_even = input.controller() % 2 == 1 | |
ui.update_checkbox("inCheckbox", value=x_even) | |
app = App(app_ui, server) | |
from datetime import date, timedelta | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("n", "Day of month", min=1, max=30, value=10), | |
ui.input_date_range("inDateRange", "Input date"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
d = date(2013, 4, input.n()) | |
ui.update_date_range( | |
"inDateRange", | |
label="Date range label " + str(input.n()), | |
start=d - timedelta(days=1), | |
end=d + timedelta(days=1), | |
min=d - timedelta(days=5), | |
max=d + timedelta(days=5), | |
) | |
app = App(app_ui, server) | |
from datetime import date, timedelta | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("n", "Day of month", min=1, max=30, value=10), | |
ui.input_date("inDate", "Input date"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
d = date(2013, 4, input.n()) | |
ui.update_date( | |
"inDate", | |
label="Date label " + str(input.n()), | |
value=d, | |
min=d - timedelta(days=3), | |
max=d + timedelta(days=3), | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.layout_sidebar( | |
ui.panel_sidebar( | |
ui.input_slider("controller", "Controller", min=1, max=3, value=1) | |
), | |
ui.panel_main( | |
ui.navset_tab_card( | |
ui.nav("Panel 1", "Panel 1 content", value="panel1"), | |
ui.nav("Panel 2", "Panel 2 content", value="panel2"), | |
ui.nav("Panel 3", "Panel 3 content", value="panel3"), | |
id="inTabset", | |
) | |
), | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
ui.update_navs("inTabset", selected="panel" + str(input.controller())) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("controller", "Controller", min=0, max=20, value=10), | |
ui.input_numeric("inNumber", "Input number", 0), | |
ui.input_numeric("inNumber2", "Input number 2", 0), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
x = input.controller() | |
ui.update_numeric("inNumber", value=x) | |
ui.update_numeric( | |
"inNumber2", | |
label="Number label " + str(x), | |
value=x, | |
min=x - 10, | |
max=x + 10, | |
step=5, | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.tags.p("The first radio button group controls the second"), | |
ui.input_radio_buttons( | |
"inRadioButtons", "Input radio buttons", ["Item A", "Item B", "Item C"] | |
), | |
ui.input_radio_buttons( | |
"inRadioButtons2", "Input radio buttons 2", ["Item A", "Item B", "Item C"] | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
x = input.inRadioButtons() | |
# Can also set the label and select items | |
ui.update_radio_buttons( | |
"inRadioButtons2", | |
label="Radio buttons label " + x, | |
choices=[x], | |
selected=x, | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.tags.p("The checkbox group controls the select input"), | |
ui.input_checkbox_group( | |
"inCheckboxGroup", "Input checkbox", ["Item A", "Item B", "Item C"] | |
), | |
ui.input_select("inSelect", "Select input", ["Item A", "Item B", "Item C"]), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
x = input.inCheckboxGroup() | |
# Can use [] to remove all choices | |
if x is None: | |
x = [] | |
elif isinstance(x, str): | |
x = [x] | |
ui.update_select( | |
"inSelect", | |
label="Select input label " + str(len(x)), | |
choices=x, | |
selected=x[len(x) - 1] if len(x) > 0 else None, | |
) | |
app = App(app_ui, server) | |
ß | |
ede fdÑZ | |
d¨ | |
¶ | |
<listcomp>z%server.<locals>._.<locals>.<listcomp> | |
◊“ÿÿ6–6≠®u©¨–6—6‘6ÿòw–'ÿ | |
Ù | |
| |
| |
| |
r | |
Nr | |
maxOptions⁄render)r | |
◊“ÿÿ6–6≠®u©¨–6—6‘6ÿêYÿ | |
Ù | |
| |
| |
| |
r | |
Ñ_ | |
| |
Ò | |
ı | |
| |
Ò | |
| |
| |
r | |
⁄ htmltoolsr | |
page_fluid⁄input_selectize⁄app_ui⁄Inputs⁄Outputs⁄Sessionr | |
1∏2» | |
Ù | |
Ä | |
ê& | |
†' | |
∞G | |
| |
| |
| |
8 | |
Äcà&ê&†–%—%‘%ÄÄÄr | |
from htmltools import HTML | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_selectize("x", "Server side selectize", choices=[], multiple=True), | |
ui.input_selectize( | |
"y", "Server side selectize with options", choices=[], multiple=True | |
), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
ui.update_selectize( | |
"x", | |
choices=[f"Foo {i}" for i in range(10000)], | |
selected=["Foo 0", "Foo 1"], | |
server=True, | |
) | |
@reactive.Effect | |
def _(): | |
ui.update_selectize( | |
"y", | |
choices=[f"Foo {i}" for i in range(10000)], | |
selected=["Foo 1"], | |
server=True, | |
options=( | |
{ | |
"maxOptions": 3, | |
"render": HTML( | |
'{option: function(item, escape) {return "<div><strong>Select " + item.label + "</strong></div>";}}' | |
), | |
} | |
), | |
) | |
app = App(app_ui, server, debug=True) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.layout_sidebar( | |
ui.panel_sidebar( | |
ui.tags.p("The first slider controls the second"), | |
ui.input_slider("control", "Controller:", min=0, max=20, value=10, step=1), | |
ui.input_slider("receive", "Receiver:", min=0, max=20, value=10, step=1), | |
), | |
ui.panel_main(), | |
) | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
val = input.control() | |
# Control the value, min, max, and step. | |
# Step size is 2 when input value is even; 1 when value is odd. | |
ui.update_slider( | |
"receive", value=val, min=int(val / 2), max=val + 4, step=(val + 1) % 2 + 1 | |
) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_slider("controller", "Controller", min=0, max=20, value=10), | |
ui.input_text("inText", "Input text"), | |
ui.input_text("inText2", "Input text 2"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
@reactive.Effect | |
def _(): | |
x = str(input.controller()) | |
# This will change the value of input$inText, based on x | |
ui.update_text("inText", value="New text " + x) | |
# Can also set the label, this time for input$inText2 | |
ui.update_text("inText2", label="New label " + x, value="New text" + x) | |
app = App(app_ui, server) | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.input_action_button("minus", "-1"), | |
ui.input_action_button("plus", "+1"), | |
ui.br(), | |
ui.output_text("value"), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
val = reactive.Value(0) | |
@reactive.Effect | |
@reactive.event(input.minus) | |
def _(): | |
newVal = val.get() - 1 | |
val.set(newVal) | |
@reactive.Effect | |
@reactive.event(input.plus) | |
def _(): | |
newVal = val.get() + 1 | |
val.set(newVal) | |
@output | |
@render.text | |
def value(): | |
return str(val.get()) | |
app = App(app_ui, server) | |
from pathlib import Path | |
from shiny import * | |
app_ui = ui.page_fluid( | |
ui.tags.link(href="css/styles.css", rel="stylesheet"), | |
ui.tags.div( | |
"If you see this text, it failed", | |
id="target", | |
style="background-color: red;", | |
), | |
ui.tags.script(src="js/changetext.js"), | |
ui.tags.div( | |
"This box should be green: ", | |
ui.tags.div( | |
id="box", | |
style="width: 100px; height:100px; border: 1px solid black;", | |
), | |
), | |
"There should be a slider below: ", | |
ui.input_slider("n", "N", min=1, max=100, value=50), | |
) | |
def server(input: Inputs, output: Outputs, session: Session): | |
pass | |
app_dir = Path(__file__).parent.resolve() | |
app = App(app_ui, server, static_assets=str(app_dir / "www")) | |
body { | |
background-color: limegreen; | |
} | |
@import url('more-styles.css'); | |
body { | |
font-size: 2rem; | |
} | |
document.getElementById("target").innerText = "If you see this text, it worked!"; | |
document.getElementById("target").style.backgroundColor = "limegreen"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment