Skip to content

Commit 811f8c7

Browse files
nstrayercpsievert
andauthored
Add busy indicators (aka spinners) (#918)
Co-authored-by: Carson <cpsievert1@gmail.com>
1 parent 7cd9e5e commit 811f8c7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+521
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### New features
1313

14+
* Added busy indicators to provide users with a visual cue when the server is busy calculating outputs or otherwise serving requests to the client. More specifically, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. Use the new `ui.busy_indicator.options()` function to customize the appearance of the busy indicators and `ui.busy_indicator.use()` to disable/enable them. (#918)
15+
1416
* Added support for creating modules using Shiny Express syntax, and using modules in Shiny Express apps. (#1220)
1517

1618
* `ui.page_*()` functions gain a `theme` argument that allows you to replace the Bootstrap CSS file with a new CSS file. `theme` can be a local CSS file, a URL, or a [shinyswatch](https://posit-dev.github.io/py-shinyswatch) theme. In Shiny Express apps, `theme` can be set via `express.ui.page_opts()`. (#1334)

docs/_quartodoc-core.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ quartodoc:
108108
- ui.include_js
109109
- ui.insert_ui
110110
- ui.remove_ui
111+
- ui.busy_indicators.use
112+
- ui.busy_indicators.options
111113
- ui.fill.as_fillable_container
112114
- ui.fill.as_fill_item
113115
- ui.fill.remove_all_fill

docs/_quartodoc-express.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ quartodoc:
165165
- name: express.ui.tags # uses tags.rst template
166166
children: embedded
167167
- express.ui.TagList # uses class.rst template
168+
- express.ui.busy_indicators.use
169+
- express.ui.busy_indicators.options
168170
# TODO: should these be included?
169171
# - express.ui.fill.as_fillable_container
170172
# - express.ui.fill.as_fill_item

examples/busy_indicators/app.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# pyright:basic
2+
import time
3+
4+
import numpy as np
5+
import seaborn as sns
6+
7+
from shiny import App, module, reactive, render, ui
8+
9+
10+
# -- Reusable card module --
11+
@module.ui
12+
def card_ui(spinner_type):
13+
return ui.card(
14+
ui.busy_indicators.options(spinner_type=spinner_type),
15+
ui.card_header("Spinner: " + spinner_type),
16+
ui.output_plot("plot"),
17+
)
18+
19+
20+
@module.server
21+
def card_server(input, output, session, rerender):
22+
@render.plot
23+
def plot():
24+
rerender()
25+
time.sleep(1)
26+
sns.lineplot(x=np.arange(100), y=np.random.randn(100))
27+
28+
29+
# -- Main app --
30+
app_ui = ui.page_fillable(
31+
ui.input_task_button("rerender", "Re-render"),
32+
ui.layout_columns(
33+
card_ui("ring", "ring"),
34+
card_ui("bars", "bars"),
35+
card_ui("dots", "dots"),
36+
card_ui("pulse", "pulse"),
37+
col_widths=6,
38+
),
39+
)
40+
41+
42+
def server(input, output, session):
43+
44+
@reactive.calc
45+
def rerender():
46+
return input.rerender()
47+
48+
card_server("ring", rerender=rerender)
49+
card_server("bars", rerender=rerender)
50+
card_server("dots", rerender=rerender)
51+
card_server("pulse", rerender=rerender)
52+
53+
54+
app = App(app_ui, server)

scripts/htmlDependencies.R

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,29 @@ ignore <- system(
344344
paste0("cd ", js_path, " && npm install && npm run build"),
345345
intern = TRUE
346346
)
347+
348+
349+
# ------------------------------------------------------------------------------
350+
message("Save spinner types")
351+
spinner_types <- Sys.glob(file.path(www_shared, "busy-indicators", "spinners", "*.svg"))
352+
spinner_types <- tools::file_path_sans_ext(basename(spinner_types))
353+
354+
template <- r"(# Generated by scripts/htmlDependencies.R: do not edit by hand
355+
356+
from __future__ import annotations
357+
358+
from typing import Literal
359+
360+
BusySpinnerType = Literal[
361+
%s
362+
])"
363+
364+
py_lines <- function(x) {
365+
x <- paste(sprintf('"%s"', x), collapse = ",\n ")
366+
paste0(" ", x, ",")
367+
}
368+
369+
writeLines(
370+
sprintf(template, py_lines(spinner_types)),
371+
file.path(shiny_path, "ui", "_busy_spinner_types.py")
372+
)

shiny/_versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
shiny_html_deps = "1.8.1.9000"
1+
shiny_html_deps = "1.8.1.9001"
22
bslib = "0.7.0.9000"
33
htmltools = "0.5.8.9000"
44
bootstrap = "5.3.1"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import os
2+
import time
3+
4+
import numpy as np
5+
import seaborn as sns
6+
7+
from shiny import App, render, ui
8+
9+
app_ui = ui.page_sidebar(
10+
ui.sidebar(
11+
ui.input_selectize(
12+
"indicator_types",
13+
"Busy indicator types",
14+
["spinners", "pulse"],
15+
multiple=True,
16+
selected=["spinners", "pulse"],
17+
),
18+
ui.download_button("download", "Download source"),
19+
),
20+
ui.card(
21+
ui.card_header(
22+
"Plot that takes a few seconds to render",
23+
ui.input_task_button("simulate", "Simulate"),
24+
class_="d-flex justify-content-between align-items-center",
25+
),
26+
ui.output_plot("plot"),
27+
),
28+
ui.busy_indicators.options(spinner_type="bars3"),
29+
ui.output_ui("indicator_types_ui"),
30+
title="Busy indicators demo",
31+
)
32+
33+
34+
def server(input):
35+
36+
@render.plot
37+
def plot():
38+
input.simulate()
39+
time.sleep(3)
40+
sns.lineplot(x=np.arange(100), y=np.random.randn(100))
41+
42+
@render.ui
43+
def indicator_types_ui():
44+
return ui.busy_indicators.use(
45+
spinners="spinners" in input.indicator_types(),
46+
pulse="pulse" in input.indicator_types(),
47+
)
48+
49+
@render.download
50+
def download():
51+
time.sleep(3)
52+
path = os.path.join(os.path.dirname(__file__), "app-core.py")
53+
return path
54+
55+
56+
app = App(app_ui, server)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import os
2+
import time
3+
4+
import numpy as np
5+
import seaborn as sns
6+
7+
from shiny.express import input, render, ui
8+
9+
ui.page_opts(title="Busy spinner demo")
10+
11+
with ui.sidebar():
12+
ui.input_selectize(
13+
"indicator_types",
14+
"Busy indicator types",
15+
["spinners", "pulse"],
16+
multiple=True,
17+
selected=["spinners", "pulse"],
18+
)
19+
20+
@render.download
21+
def download():
22+
time.sleep(3)
23+
path = os.path.join(os.path.dirname(__file__), "app-express.py")
24+
return path
25+
26+
27+
with ui.card():
28+
ui.card_header(
29+
"Plot that takes a few seconds to render",
30+
ui.input_task_button("simulate", "Simulate"),
31+
class_="d-flex justify-content-between align-items-center",
32+
)
33+
34+
@render.plot
35+
def plot():
36+
input.simulate()
37+
time.sleep(3)
38+
sns.lineplot(x=np.arange(100), y=np.random.randn(100))
39+
40+
41+
ui.busy_indicators.options(spinner_type="bars3")
42+
43+
44+
@render.ui
45+
def indicator_types_ui():
46+
return ui.busy_indicators.use(
47+
spinners="spinners" in input.indicator_types(),
48+
pulse="pulse" in input.indicator_types(),
49+
)

shiny/express/ui/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
fill,
3434
)
3535

36+
from ...ui import (
37+
busy_indicators,
38+
)
39+
3640
from ...ui import (
3741
AccordionPanel,
3842
AnimationOptions,
@@ -176,6 +180,7 @@
176180
"strong",
177181
"tags",
178182
# Submodules
183+
"busy_indicators",
179184
"fill",
180185
# Imports from ...ui
181186
"AccordionPanel",

shiny/html_dependencies.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,26 @@
44

55
from htmltools import HTMLDependency
66

7+
from . import __version__
8+
from .ui._html_deps_py_shiny import busy_indicators_dep
9+
710

811
def shiny_deps() -> list[HTMLDependency]:
912
deps = [
1013
HTMLDependency(
1114
name="shiny",
12-
version="0.0.1",
15+
version=__version__,
1316
source={"package": "shiny", "subdir": "www/shared/"},
1417
script={"src": "shiny.js"},
1518
stylesheet={"href": "shiny.min.css"},
16-
)
19+
),
20+
busy_indicators_dep(),
1721
]
1822
if os.getenv("SHINY_DEV_MODE") == "1":
1923
deps.append(
2024
HTMLDependency(
2125
"shiny-devmode",
22-
version="0.0.1",
26+
version=__version__,
2327
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
2428
)
2529
)

0 commit comments

Comments
 (0)