Skip to content

Commit

Permalink
Add busy indicators (aka spinners) (#918)
Browse files Browse the repository at this point in the history
Co-authored-by: Carson <[email protected]>
  • Loading branch information
nstrayer and cpsievert authored May 10, 2024
1 parent 7cd9e5e commit 811f8c7
Show file tree
Hide file tree
Showing 43 changed files with 521 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* 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)

* Added support for creating modules using Shiny Express syntax, and using modules in Shiny Express apps. (#1220)

* `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)
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ quartodoc:
- ui.include_js
- ui.insert_ui
- ui.remove_ui
- ui.busy_indicators.use
- ui.busy_indicators.options
- ui.fill.as_fillable_container
- ui.fill.as_fill_item
- ui.fill.remove_all_fill
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-express.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ quartodoc:
- name: express.ui.tags # uses tags.rst template
children: embedded
- express.ui.TagList # uses class.rst template
- express.ui.busy_indicators.use
- express.ui.busy_indicators.options
# TODO: should these be included?
# - express.ui.fill.as_fillable_container
# - express.ui.fill.as_fill_item
Expand Down
54 changes: 54 additions & 0 deletions examples/busy_indicators/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# pyright:basic
import time

import numpy as np
import seaborn as sns

from shiny import App, module, reactive, render, ui


# -- Reusable card module --
@module.ui
def card_ui(spinner_type):
return ui.card(
ui.busy_indicators.options(spinner_type=spinner_type),
ui.card_header("Spinner: " + spinner_type),
ui.output_plot("plot"),
)


@module.server
def card_server(input, output, session, rerender):
@render.plot
def plot():
rerender()
time.sleep(1)
sns.lineplot(x=np.arange(100), y=np.random.randn(100))


# -- Main app --
app_ui = ui.page_fillable(
ui.input_task_button("rerender", "Re-render"),
ui.layout_columns(
card_ui("ring", "ring"),
card_ui("bars", "bars"),
card_ui("dots", "dots"),
card_ui("pulse", "pulse"),
col_widths=6,
),
)


def server(input, output, session):

@reactive.calc
def rerender():
return input.rerender()

card_server("ring", rerender=rerender)
card_server("bars", rerender=rerender)
card_server("dots", rerender=rerender)
card_server("pulse", rerender=rerender)


app = App(app_ui, server)
26 changes: 26 additions & 0 deletions scripts/htmlDependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,29 @@ ignore <- system(
paste0("cd ", js_path, " && npm install && npm run build"),
intern = TRUE
)


# ------------------------------------------------------------------------------
message("Save spinner types")
spinner_types <- Sys.glob(file.path(www_shared, "busy-indicators", "spinners", "*.svg"))
spinner_types <- tools::file_path_sans_ext(basename(spinner_types))

template <- r"(# Generated by scripts/htmlDependencies.R: do not edit by hand
from __future__ import annotations
from typing import Literal
BusySpinnerType = Literal[
%s
])"

py_lines <- function(x) {
x <- paste(sprintf('"%s"', x), collapse = ",\n ")
paste0(" ", x, ",")
}

writeLines(
sprintf(template, py_lines(spinner_types)),
file.path(shiny_path, "ui", "_busy_spinner_types.py")
)
2 changes: 1 addition & 1 deletion shiny/_versions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
shiny_html_deps = "1.8.1.9000"
shiny_html_deps = "1.8.1.9001"
bslib = "0.7.0.9000"
htmltools = "0.5.8.9000"
bootstrap = "5.3.1"
Expand Down
56 changes: 56 additions & 0 deletions shiny/api-examples/busy_indicators/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import time

import numpy as np
import seaborn as sns

from shiny import App, render, ui

app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_selectize(
"indicator_types",
"Busy indicator types",
["spinners", "pulse"],
multiple=True,
selected=["spinners", "pulse"],
),
ui.download_button("download", "Download source"),
),
ui.card(
ui.card_header(
"Plot that takes a few seconds to render",
ui.input_task_button("simulate", "Simulate"),
class_="d-flex justify-content-between align-items-center",
),
ui.output_plot("plot"),
),
ui.busy_indicators.options(spinner_type="bars3"),
ui.output_ui("indicator_types_ui"),
title="Busy indicators demo",
)


def server(input):

@render.plot
def plot():
input.simulate()
time.sleep(3)
sns.lineplot(x=np.arange(100), y=np.random.randn(100))

@render.ui
def indicator_types_ui():
return ui.busy_indicators.use(
spinners="spinners" in input.indicator_types(),
pulse="pulse" in input.indicator_types(),
)

@render.download
def download():
time.sleep(3)
path = os.path.join(os.path.dirname(__file__), "app-core.py")
return path


app = App(app_ui, server)
49 changes: 49 additions & 0 deletions shiny/api-examples/busy_indicators/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os
import time

import numpy as np
import seaborn as sns

from shiny.express import input, render, ui

ui.page_opts(title="Busy spinner demo")

with ui.sidebar():
ui.input_selectize(
"indicator_types",
"Busy indicator types",
["spinners", "pulse"],
multiple=True,
selected=["spinners", "pulse"],
)

@render.download
def download():
time.sleep(3)
path = os.path.join(os.path.dirname(__file__), "app-express.py")
return path


with ui.card():
ui.card_header(
"Plot that takes a few seconds to render",
ui.input_task_button("simulate", "Simulate"),
class_="d-flex justify-content-between align-items-center",
)

@render.plot
def plot():
input.simulate()
time.sleep(3)
sns.lineplot(x=np.arange(100), y=np.random.randn(100))


ui.busy_indicators.options(spinner_type="bars3")


@render.ui
def indicator_types_ui():
return ui.busy_indicators.use(
spinners="spinners" in input.indicator_types(),
pulse="pulse" in input.indicator_types(),
)
5 changes: 5 additions & 0 deletions shiny/express/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
fill,
)

from ...ui import (
busy_indicators,
)

from ...ui import (
AccordionPanel,
AnimationOptions,
Expand Down Expand Up @@ -176,6 +180,7 @@
"strong",
"tags",
# Submodules
"busy_indicators",
"fill",
# Imports from ...ui
"AccordionPanel",
Expand Down
10 changes: 7 additions & 3 deletions shiny/html_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,26 @@

from htmltools import HTMLDependency

from . import __version__
from .ui._html_deps_py_shiny import busy_indicators_dep


def shiny_deps() -> list[HTMLDependency]:
deps = [
HTMLDependency(
name="shiny",
version="0.0.1",
version=__version__,
source={"package": "shiny", "subdir": "www/shared/"},
script={"src": "shiny.js"},
stylesheet={"href": "shiny.min.css"},
)
),
busy_indicators_dep(),
]
if os.getenv("SHINY_DEV_MODE") == "1":
deps.append(
HTMLDependency(
"shiny-devmode",
version="0.0.1",
version=__version__,
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
)
)
Expand Down
9 changes: 9 additions & 0 deletions shiny/session/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,15 @@ async def uploadEnd(job_id: str, input_id: str) -> None:
# ==========================================================================
async def _handle_request(
self, request: Request, action: str, subpath: Optional[str]
) -> ASGIApp:
self._send_message_sync({"busy": "busy"})
try:
return await self._handle_request_impl(request, action, subpath)
finally:
self._send_message_sync({"busy": "idle"})

async def _handle_request_impl(
self, request: Request, action: str, subpath: Optional[str]
) -> ASGIApp:
if action == "upload" and request.method == "POST":
if subpath is None or subpath == "":
Expand Down
4 changes: 4 additions & 0 deletions shiny/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
# Expose the fill module for extended usage: ex: ui.fill.as_fill_item(x).
from . import fill

# Export busy_indicators module
from . import busy_indicators

from ._accordion import (
AccordionPanel,
accordion,
Expand Down Expand Up @@ -355,6 +358,7 @@
"em",
"hr",
# Submodules
"busy_indicators",
"fill",
# utils
"js_eval",
Expand Down
20 changes: 20 additions & 0 deletions shiny/ui/_busy_spinner_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by scripts/htmlDependencies.R: do not edit by hand

from __future__ import annotations

from typing import Literal

BusySpinnerType = Literal[
"bars",
"bars2",
"bars3",
"dots",
"dots2",
"dots3",
"pulse",
"pulse2",
"pulse3",
"ring",
"ring2",
"ring3",
]
13 changes: 13 additions & 0 deletions shiny/ui/_html_deps_py_shiny.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from htmltools import HTMLDependency

from .. import __version__
from . import busy_indicators

"""
HTML dependencies for internal dependencies such as dataframe or text area's autoresize.
Expand Down Expand Up @@ -52,3 +53,15 @@ def spin_dependency() -> HTMLDependency:
source={"package": "shiny", "subdir": "www/shared/py-shiny/spin"},
stylesheet={"href": "spin.css"},
)


def busy_indicators_dep() -> HTMLDependency:
return HTMLDependency(
"shiny-busy-indicators",
__version__,
source={"package": "shiny", "subdir": "www/shared/busy-indicators"},
stylesheet={"href": "busy-indicators.css"},
script={"src": "busy-indicators.js"},
head=busy_indicators.use(), # Enable busy indicators by default.
all_files=True,
)
Loading

0 comments on commit 811f8c7

Please sign in to comment.