diff --git a/docs/_quartodoc-core.yml b/docs/_quartodoc-core.yml index 510cf9190..73f0f2471 100644 --- a/docs/_quartodoc-core.yml +++ b/docs/_quartodoc-core.yml @@ -328,58 +328,6 @@ quartodoc: - types.SilentException - types.SilentCancelOutputException - types.SafeException - - title: Shiny Express - desc: Functions for Shiny Express applications - contents: - - kind: page - path: ContextManagerComponents - summary: - name: "Context manager components" - desc: "" - flatten: true - contents: - - express.ui.sidebar - - express.ui.layout_sidebar - - express.ui.layout_column_wrap - - express.ui.layout_columns - - express.ui.card - - express.ui.accordion - - express.ui.accordion_panel - - express.ui.nav_panel - - express.ui.nav_control - - express.ui.nav_menu - - express.ui.navset_bar - - express.ui.navset_card_pill - - express.ui.navset_card_tab - - express.ui.navset_card_underline - - express.ui.navset_hidden - - express.ui.navset_pill - - express.ui.navset_pill_list - - express.ui.navset_tab - - express.ui.navset_underline - - express.ui.value_box - - express.ui.panel_title - - express.ui.panel_well - - express.ui.panel_conditional - - express.ui.panel_fixed - - express.ui.panel_absolute - - kind: page - path: PageFunctions - summary: - name: "Page functions" - desc: "" - flatten: true - contents: - - express.ui.page_opts - - kind: page - path: DisplayFunctions - summary: - name: "Display functions" - desc: "" - flatten: true - contents: - - express.ui.hold - - express.expressify - title: Deprecated desc: "" contents: diff --git a/docs/_renderer.py b/docs/_renderer.py index 92930c8ee..3fe496737 100644 --- a/docs/_renderer.py +++ b/docs/_renderer.py @@ -271,10 +271,6 @@ def read_file(file: str | Path, root_dir: str | Path | None = None) -> FileConte def check_if_missing_expected_example(el, converted): - if os.environ.get("SHINY_MODE", "core") == "express": - # TODO: remove once we are done porting express examples - return - if re.search(r"(^|\n)#{2,6} Examples\n", converted): # Manually added examples are fine return @@ -287,7 +283,12 @@ def is_no_ex_decorator(x): if x == "no_example()": return True - return x == f'no_example("{os.environ.get("SHINY_MODE", "core")}")' + no_ex_decorators = [ + f'no_example("{os.environ.get("SHINY_MODE", "core")}")', + f"no_example('{os.environ.get('SHINY_MODE', 'core')}')", + ] + + return x in no_ex_decorators if hasattr(el, "decorators") and any( [is_no_ex_decorator(d.value.canonical_name) for d in el.decorators] @@ -304,8 +305,7 @@ def is_no_ex_decorator(x): # In practice, this covers methods of exported classes (class still needs ex) return - # TODO: Remove shiny.express from no_req_examples when we have examples ready - no_req_examples = ["shiny.express", "shiny.experimental"] + no_req_examples = ["shiny.experimental"] if any([el.target_path.startswith(mod) for mod in no_req_examples]): return diff --git a/shiny/_docstring.py b/shiny/_docstring.py index 10281861f..df18c5208 100644 --- a/shiny/_docstring.py +++ b/shiny/_docstring.py @@ -138,7 +138,8 @@ def _(func: F) -> F: app_file_name = app_file or "app.py" try: example_file = app_choose_core_or_express( - os.path.join(example_dir, app_file_name) + os.path.join(example_dir, app_file_name), + mode="express" if "shiny/express/" in func_dir else None, ) except ExampleNotFoundException as e: file = "shiny/" + func_dir.split("shiny/")[1] @@ -248,10 +249,17 @@ def __init__( super().__init__(file_names, dir, "express") -def app_choose_core_or_express(app_path: Optional[str] = None) -> str: +def app_choose_core_or_express( + app_path: Optional[str] = None, + mode: Optional[Literal["express", "core"]] = None, +) -> str: app_path = app_path or "app.py" - if os.environ.get("SHINY_MODE") == "express": + if mode is None: + mode_env = os.environ.get("SHINY_MODE", "core") + mode = "express" if mode_env == "express" else "core" + + if mode == "express": if is_express_app(app_path): return app_path diff --git a/shiny/api-examples/Renderer/app-core.py b/shiny/api-examples/Renderer/app-core.py index 1877778c5..3931c17e8 100644 --- a/shiny/api-examples/Renderer/app-core.py +++ b/shiny/api-examples/Renderer/app-core.py @@ -1,157 +1,21 @@ from __future__ import annotations -from typing import Literal, Optional +# Import the custom renderer implementations +from renderers import render_capitalize, render_upper from shiny import App, Inputs, Outputs, Session, ui -from shiny.render.renderer import Renderer, ValueFn - -####### -# Start of package author code -####### - - -class render_capitalize(Renderer[str]): - # The documentation for the class will be displayed when the user hovers over the - # decorator when **no** parenthesis are used. Ex: `@render_capitalize` - # If no documentation is supplied to the `__init__()` method, then this - # documentation will be displayed when parenthesis are used on the decorator. - """ - Render capitalize class documentation goes here. - """ - - to_case: Literal["upper", "lower", "ignore"] - """ - The case to render the value in. - """ - placeholder: bool - """ - Whether to render a placeholder value. (Defaults to `True`) - """ - - def auto_output_ui(self): - """ - Express UI for the renderer - """ - return ui.output_text_verbatim(self.output_name, placeholder=self.placeholder) - - def __init__( - self, - _fn: Optional[ValueFn[str]] = None, - *, - to_case: Literal["upper", "lower", "ignore"] = "upper", - placeholder: bool = True, - ) -> None: - # If a different set of documentation is supplied to the `__init__` method, - # then this documentation will be displayed when parenthesis are used on the decorator. - # Ex: `@render_capitalize()` - """ - Render capitalize documentation goes here. - - It is a good idea to talk about parameters here! - - Parameters - ---------- - to_case - The case to render the value. (`"upper"`) - - Options: - - `"upper"`: Render the value in upper case. - - `"lower"`: Render the value in lower case. - - `"ignore"`: Do not alter the case of the value. - - placeholder - Whether to render a placeholder value. (`True`) - """ - # Do not pass params - super().__init__(_fn) - self.to_case = to_case - - async def render(self) -> str | None: - value = await self.fn() - if value is None: - # If `None` is returned, then do not render anything. - return None - - ret = str(value) - if self.to_case == "upper": - return ret.upper() - if self.to_case == "lower": - return ret.lower() - if self.to_case == "ignore": - return ret - raise ValueError(f"Invalid value for `to_case`: {self.to_case}") - - -class render_upper(Renderer[str]): - """ - Minimal capitalize string transformation renderer. - - No parameters are supplied to this renderer. This allows us to skip the `__init__()` - method and `__init__()` documentation. If you hover over this decorator with and - without parenthesis, you will see this documentation in both situations. - - Note: This renderer is equivalent to `render_capitalize(to="upper")`. - """ - - def auto_output_ui(self): - """ - Express UI for the renderer - """ - return ui.output_text_verbatim(self.output_name, placeholder=True) - - async def transform(self, value: str) -> str: - """ - Transform the value to upper case. - - This method is shorthand for the default `render()` method. It is useful to - transform non-`None` values. (Any `None` value returned by the app author will - be forwarded to the browser.) - - Parameters - ---------- - value - The a non-`None` value to transform. - - Returns - ------- - str - The transformed value. (Must be a subset of `Jsonifiable`.) - """ - - return str(value).upper() - - -####### -# End of package author code -####### - - -####### -# Start of app author code -####### - - -def text_row(id: str, label: str): - return ui.tags.tr( - ui.tags.td(f"{label}:"), - ui.tags.td(ui.output_text_verbatim(id, placeholder=True)), - ) - return ui.row( - ui.column(6, f"{id}:"), - ui.column(6, ui.output_text_verbatim(id, placeholder=True)), - ) - app_ui = ui.page_fluid( ui.h1("Capitalization renderer"), ui.input_text("caption", "Caption:", "Data summary"), - ui.tags.table( - text_row("upper", "@render_upper"), - text_row("upper_with_paren", "@render_upper()"), - # - text_row("cap_upper", "@render_capitalize"), - text_row("cap_lower", "@render_capitalize(to='lower')"), - ), + "@render_upper: ", + ui.output_text_verbatim("upper", placeholder=True), + "@render_upper(): ", + ui.output_text_verbatim("upper_with_paren", placeholder=True), + "@render_capitalize: ", + ui.output_text_verbatim("cap_upper", placeholder=True), + "@render_capitalize(to='lower'): ", + ui.output_text_verbatim("cap_lower", placeholder=True), ) diff --git a/shiny/api-examples/Renderer/app-express.py b/shiny/api-examples/Renderer/app-express.py new file mode 100644 index 000000000..318603dc5 --- /dev/null +++ b/shiny/api-examples/Renderer/app-express.py @@ -0,0 +1,43 @@ +# Import the custom renderer implementations +from renderers import render_capitalize, render_upper + +from shiny.express import input, ui + +ui.h1("Capitalization renderer") +ui.input_text("caption", "Caption:", "Data summary") + +"@render_upper:" + + +# Hovering over `@render_upper` will display the class documentation +@render_upper +def upper(): + return input.caption() + + +"@render_upper():" + + +# Hovering over `@render_upper` will display the class documentation as there is no +# `__init__()` documentation +@render_upper() +def upper_with_paren(): + return input.caption() + + +"@render_capitalize:" + + +# Hovering over `@render_capitalize` will display the class documentation +@render_capitalize +def cap_upper(): + return input.caption() + + +"@render_capitalize(to='lower'): " + + +# Hovering over `@render_capitalize` will display the `__init__()` documentation +@render_capitalize(to_case="lower") +def cap_lower(): + return input.caption() diff --git a/shiny/api-examples/Renderer/renderers.py b/shiny/api-examples/Renderer/renderers.py new file mode 100644 index 000000000..a46dbc7be --- /dev/null +++ b/shiny/api-examples/Renderer/renderers.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from typing import Literal, Optional + +from shiny.render.renderer import Renderer, ValueFn +from shiny.ui import output_text_verbatim + + +class render_capitalize(Renderer[str]): + # The documentation for the class will be displayed when the user hovers over the + # decorator when **no** parenthesis are used. Ex: `@render_capitalize` + # If no documentation is supplied to the `__init__()` method, then this + # documentation will be displayed when parenthesis are used on the decorator. + """ + Render capitalize class documentation goes here. + """ + + to_case: Literal["upper", "lower", "ignore"] + """ + The case to render the value in. + """ + placeholder: bool + """ + Whether to render a placeholder value. (Defaults to `True`) + """ + + def auto_output_ui(self): + """ + Express UI for the renderer + """ + return output_text_verbatim(self.output_id, placeholder=True) + + def __init__( + self, + _fn: Optional[ValueFn[str]] = None, + *, + to_case: Literal["upper", "lower", "ignore"] = "upper", + placeholder: bool = True, + ) -> None: + # If a different set of documentation is supplied to the `__init__` method, + # then this documentation will be displayed when parenthesis are used on the decorator. + # Ex: `@render_capitalize()` + """ + Render capitalize documentation goes here. + + It is a good idea to talk about parameters here! + + Parameters + ---------- + to_case + The case to render the value. (`"upper"`) + + Options: + - `"upper"`: Render the value in upper case. + - `"lower"`: Render the value in lower case. + - `"ignore"`: Do not alter the case of the value. + + placeholder + Whether to render a placeholder value. (`True`) + """ + # Do not pass params + super().__init__(_fn) + self.to_case = to_case + self.placeholder = placeholder + + async def render(self) -> str | None: + value = await self.fn() + if value is None: + # If `None` is returned, then do not render anything. + return None + + ret = str(value) + if self.to_case == "upper": + return ret.upper() + if self.to_case == "lower": + return ret.lower() + if self.to_case == "ignore": + return ret + raise ValueError(f"Invalid value for `to_case`: {self.to_case}") + + +class render_upper(Renderer[str]): + """ + Minimal capitalize string transformation renderer. + + No parameters are supplied to this renderer. This allows us to skip the `__init__()` + method and `__init__()` documentation. If you hover over this decorator with and + without parenthesis, you will see this documentation in both situations. + + Note: This renderer is equivalent to `render_capitalize(to="upper")`. + """ + + def auto_output_ui(self): + """ + Express UI for the renderer + """ + return output_text_verbatim(self.output_id, placeholder=True) + + async def transform(self, value: str) -> str: + """ + Transform the value to upper case. + + This method is shorthand for the default `render()` method. It is useful to + transform non-`None` values. (Any `None` value returned by the app author will + be forwarded to the browser.) + + Parameters + ---------- + value + The a non-`None` value to transform. + + Returns + ------- + str + The transformed value. (Must be a subset of `Jsonifiable`.) + """ + + return str(value).upper() diff --git a/shiny/api-examples/SafeException/app-express.py b/shiny/api-examples/SafeException/app-express.py new file mode 100644 index 000000000..18f6be238 --- /dev/null +++ b/shiny/api-examples/SafeException/app-express.py @@ -0,0 +1,14 @@ +from shiny.express import render +from shiny.types import SafeException + + +@render.ui +def safe(): + # This error _won't_ be sanitized when deployed + raise SafeException("This is a safe exception") + + +@render.ui +def unsafe(): + # This error _will_ be sanitized when deployed + raise Exception("This is an unsafe exception") diff --git a/shiny/api-examples/SilentCancelOutputException/app-express.py b/shiny/api-examples/SilentCancelOutputException/app-express.py new file mode 100644 index 000000000..51deac1d9 --- /dev/null +++ b/shiny/api-examples/SilentCancelOutputException/app-express.py @@ -0,0 +1,16 @@ +from shiny.express import input, render, ui +from shiny.types import SilentCancelOutputException + +ui.input_text( + "txt", + "Delete the input text completely: it won't get removed below the input", + "Some text", + width="400px", +) + + +@render.ui +def txt_out(): + if not input.txt(): + raise SilentCancelOutputException() + return "Your input: " + input.txt() diff --git a/shiny/api-examples/SilentException/app-express.py b/shiny/api-examples/SilentException/app-express.py new file mode 100644 index 000000000..6673b8841 --- /dev/null +++ b/shiny/api-examples/SilentException/app-express.py @@ -0,0 +1,15 @@ +from shiny.express import input, render, ui +from shiny.types import SilentException + +ui.input_text( + "txt", + "Enter text to see it displayed below the input", + width="400px", +) + + +@render.ui +def txt_out(): + if not input.txt(): + raise SilentException() + return "Your input: " + input.txt() diff --git a/shiny/api-examples/accordion/app-core.py b/shiny/api-examples/accordion/app-core.py index 5f278f6d6..4c91ec8d5 100644 --- a/shiny/api-examples/accordion/app-core.py +++ b/shiny/api-examples/accordion/app-core.py @@ -1,4 +1,4 @@ -from shiny import App, Inputs, Outputs, Session, reactive, render, ui +from shiny import App, Inputs, Outputs, Session, render, ui def make_items(): @@ -34,17 +34,13 @@ def make_items(): def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect - def _(): - print(input.acc()) + @render.text + def acc_single_val(): + return "input.acc_single(): " + str(input.acc_single()) @render.text def acc_multiple_val(): return "input.acc_multiple(): " + str(input.acc_multiple()) - @render.text - def acc_single_val(): - return "input.acc_single(): " + str(input.acc_single()) - app = App(app_ui, server) diff --git a/shiny/api-examples/accordion/app-express.py b/shiny/api-examples/accordion/app-express.py new file mode 100644 index 000000000..003372d90 --- /dev/null +++ b/shiny/api-examples/accordion/app-express.py @@ -0,0 +1,31 @@ +from shiny.express import expressify, input, render, ui + + +@expressify +def my_accordion(**kwargs): + with ui.accordion(**kwargs): + for letter in "ABCDE": + with ui.accordion_panel(f"Section {letter}"): + f"Some narrative for section {letter}" + + +ui.markdown("#### Single-select accordion") + +my_accordion(multiple=False, id="acc_single") + + +@render.code +def acc_single_val(): + return "input.acc_single(): " + str(input.acc_single()) + + +ui.br() + +ui.markdown("#### Multi-select accordion") + +my_accordion(multiple=True, id="acc_multiple") + + +@render.code +def acc_multiple_val(): + return "input.acc_multiple(): " + str(input.acc_multiple()) diff --git a/shiny/api-examples/accordion_panel/app-express.py b/shiny/api-examples/accordion_panel/app-express.py new file mode 100644 index 000000000..115a3767b --- /dev/null +++ b/shiny/api-examples/accordion_panel/app-express.py @@ -0,0 +1,11 @@ +from shiny.express import input, render, ui + +with ui.accordion(id="acc"): + for letter in "ABCDE": + with ui.accordion_panel(f"Section {letter}"): + f"Some narrative for section {letter}" + + +@render.code +def acc_val(): + return "input.acc(): " + str(input.acc()) diff --git a/shiny/api-examples/as_fill_item/app-core.py b/shiny/api-examples/as_fill_item/app-core.py index 20d0e748c..5dcf37bd8 100644 --- a/shiny/api-examples/as_fill_item/app-core.py +++ b/shiny/api-examples/as_fill_item/app-core.py @@ -44,7 +44,7 @@ def outer_inner() -> htmltools.Tag: * the item must have `as_fill_item()` be called on it * the parent container must have `as_fillable_container()` called on it - Iff both methods are called, the inner child will naturally expand into its parent container. + If both methods are called, the inner child will naturally expand into its parent container. """ ), ui.row( diff --git a/shiny/api-examples/as_fill_item/app-express.py b/shiny/api-examples/as_fill_item/app-express.py new file mode 100644 index 000000000..167d8b2c4 --- /dev/null +++ b/shiny/api-examples/as_fill_item/app-express.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from htmltools import Tag, css + +from shiny.express import ui +from shiny.express.ui import fill + +ui.markdown( + """\ + # `as_fill_item()` + + For an item to fill its parent element, + * the item must have `as_fill_item()` be called on it + * the parent container must have `as_fillable_container()` called on it + + If both methods are called, the inner child will naturally expand into its parent container. + """ +) + + +def outer_inner() -> Tag: + inner = ui.div( + id="inner", + style=css( + height="200px", + border="3px blue solid", + ), + ) + outer = ui.div( + inner, + id="outer", + style=css( + height="300px", + border="3px red solid", + ), + ) + return outer + + +outer0 = outer_inner() + +outer1 = outer_inner() +outer1.children[0] = fill.as_fill_item(outer1.children[0]) + +outer2 = outer_inner() +outer2 = fill.as_fillable_container(outer2) +outer2.children[0] = fill.as_fill_item(outer2.children[0]) + +with ui.layout_columns(): + ui.markdown("##### Default behavior") + ui.markdown("##### `as_fill_item(blue)`") + ui.markdown("##### `as_fill_item(blue)` + `as_fillable_container(red)`") + +with ui.layout_columns(): + outer0 + outer1 + outer2 diff --git a/shiny/api-examples/as_fillable_container/app-core.py b/shiny/api-examples/as_fillable_container/app-core.py index a327f4d18..2c9daecbe 100644 --- a/shiny/api-examples/as_fillable_container/app-core.py +++ b/shiny/api-examples/as_fillable_container/app-core.py @@ -43,7 +43,7 @@ def outer_inner() -> htmltools.Tag: * the item must have `as_fill_item()` be called on it * the parent container must have `as_fillable_container()` called on it - Iff both methods are called, the inner child will naturally expand into its parent container. + If both methods are called, the inner child will naturally expand into its parent container. """ ), ui.row( diff --git a/shiny/api-examples/as_fillable_container/app-express.py b/shiny/api-examples/as_fillable_container/app-express.py new file mode 100644 index 000000000..f9db89e6c --- /dev/null +++ b/shiny/api-examples/as_fillable_container/app-express.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from htmltools import Tag, css + +from shiny.express import ui +from shiny.express.ui import fill + +ui.markdown( + """\ + # `as_fillable_container()` + + For an item to fill its parent element, + * the item must have `as_fill_item()` be called on it + * the parent container must have `as_fillable_container()` called on it + + If both methods are called, the inner child will naturally expand into its parent container. + """ +) + + +def outer_inner() -> Tag: + inner = ui.div( + id="inner", + style=css( + height="200px", + border="3px blue solid", + ), + ) + outer = ui.div( + inner, + id="outer", + style=css( + height="300px", + border="3px red solid", + ), + ) + return outer + + +outer0 = outer_inner() + +outer1 = outer_inner() +outer1.children[0] = fill.as_fill_item(outer1.children[0]) + +outer2 = outer_inner() +outer2 = fill.as_fillable_container(outer2) +outer2.children[0] = fill.as_fill_item(outer2.children[0]) + +with ui.layout_columns(): + ui.markdown("##### Default behavior") + ui.markdown("##### `as_fill_item(blue)`") + ui.markdown("##### `as_fill_item(blue)` + `as_fillable_container(red)`") + +with ui.layout_columns(): + outer0 + outer1 + outer2 diff --git a/shiny/api-examples/card/app-express.py b/shiny/api-examples/card/app-express.py new file mode 100644 index 000000000..5a78dbe5b --- /dev/null +++ b/shiny/api-examples/card/app-express.py @@ -0,0 +1,7 @@ +from shiny.express import ui + +with ui.card(full_screen=True): + ui.card_header("This is the header") + ui.p("This is the body.") + ui.p("This is still the body.") + ui.card_footer("This is the footer") diff --git a/shiny/api-examples/card_footer/app-express.py b/shiny/api-examples/card_footer/app-express.py new file mode 100644 index 000000000..5a78dbe5b --- /dev/null +++ b/shiny/api-examples/card_footer/app-express.py @@ -0,0 +1,7 @@ +from shiny.express import ui + +with ui.card(full_screen=True): + ui.card_header("This is the header") + ui.p("This is the body.") + ui.p("This is still the body.") + ui.card_footer("This is the footer") diff --git a/shiny/api-examples/card_header/app-express.py b/shiny/api-examples/card_header/app-express.py new file mode 100644 index 000000000..5a78dbe5b --- /dev/null +++ b/shiny/api-examples/card_header/app-express.py @@ -0,0 +1,7 @@ +from shiny.express import ui + +with ui.card(full_screen=True): + ui.card_header("This is the header") + ui.p("This is the body.") + ui.p("This is still the body.") + ui.card_footer("This is the footer") diff --git a/shiny/api-examples/close/app-express.py b/shiny/api-examples/close/app-express.py new file mode 100644 index 000000000..ef4e29930 --- /dev/null +++ b/shiny/api-examples/close/app-express.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from shiny import reactive +from shiny.express import input, session, ui + +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 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() diff --git a/shiny/api-examples/data_frame/app-core.py b/shiny/api-examples/data_frame/app-core.py index 6cb007801..b2b0d1d76 100644 --- a/shiny/api-examples/data_frame/app-core.py +++ b/shiny/api-examples/data_frame/app-core.py @@ -1,9 +1,8 @@ import pandas # noqa: F401 (this line needed for Shinylive to load plotly.express) import plotly.express as px -import plotly.graph_objs as go from shinywidgets import output_widget, render_widget -from shiny import App, reactive, render, req, session, ui +from shiny import App, reactive, render, req, ui # Load the Gapminder dataset df = px.data.gapminder() @@ -26,24 +25,14 @@ app_ui = ui.page_fillable( {"class": "p-3"}, - ui.p( - ui.strong("Instructions:"), - " Select one or more countries in the table below to see more information.", + ui.markdown( + "**Instructions**: Select one or more countries in the table below to see more information." ), - ui.layout_column_wrap( - ui.card( - ui.output_data_frame("summary_data"), - ), - ui.layout_column_wrap( - ui.card( - output_widget("country_detail_pop", height="100%"), - ), - ui.card( - output_widget("country_detail_percap", height="100%"), - ), - width=1 / 2, - ), - width=1, + ui.layout_columns( + ui.card(ui.output_data_frame("summary_data"), height="400px"), + ui.card(output_widget("country_detail_pop"), height="400px"), + ui.card(output_widget("country_detail_percap"), height="400px"), + col_widths=[12, 6, 6], ), ) @@ -51,14 +40,9 @@ def server(input, output, session): @render.data_frame def summary_data(): - return render.DataGrid( - summary_df.round(2), - row_selection_mode="multiple", - width="100%", - height="100%", - ) + return render.DataGrid(summary_df.round(2), row_selection_mode="multiple") - @reactive.Calc + @reactive.calc def filtered_df(): # input.summary_data_selected_rows() is a tuple, so we must convert it to list, # as that's what Pandas requires for indexing. @@ -69,64 +53,23 @@ def filtered_df(): @render_widget def country_detail_pop(): - # Create the plot - fig = px.line( + return px.line( filtered_df(), x="year", y="pop", color="country", title="Population Over Time", ) - widget = go.FigureWidget(fig) - - @synchronize_size("country_detail_pop") - def on_size_changed(width, height): - widget.layout.width = width - widget.layout.height = height - - return widget @render_widget def country_detail_percap(): - # Create the plot - fig = px.line( + return px.line( filtered_df(), x="year", y="gdpPercap", color="country", title="GDP per Capita Over Time", ) - widget = go.FigureWidget(fig) - - @synchronize_size("country_detail_percap") - def on_size_changed(width, height): - widget.layout.width = width - widget.layout.height = height - - return widget - - -# This is a hacky workaround to help Plotly plots automatically -# resize to fit their container. In the future we'll have a -# built-in solution for this. -def synchronize_size(output_id): - def wrapper(func): - input = session.get_current_session().input - - @reactive.Effect - def size_updater(): - func( - input[f".clientdata_output_{output_id}_width"](), - input[f".clientdata_output_{output_id}_height"](), - ) - - # When the output that we're synchronizing to is invalidated, - # clean up the size_updater Effect. - reactive.get_current_context().on_invalidate(size_updater.destroy) - - return size_updater - - return wrapper app = App(app_ui, server) diff --git a/shiny/api-examples/data_frame/app-express.py b/shiny/api-examples/data_frame/app-express.py new file mode 100644 index 000000000..638590cb8 --- /dev/null +++ b/shiny/api-examples/data_frame/app-express.py @@ -0,0 +1,74 @@ +import pandas # noqa: F401 (this line needed for Shinylive to load plotly.express) +import plotly.express as px +from shinywidgets import render_widget + +from shiny import reactive, req +from shiny.express import input, render, ui + +# Load the Gapminder dataset +df = px.data.gapminder() + +# Prepare a summary DataFrame +summary_df = ( + df.groupby("country") + .agg( + { + "pop": ["min", "max", "mean"], + "lifeExp": ["min", "max", "mean"], + "gdpPercap": ["min", "max", "mean"], + } + ) + .reset_index() +) + +summary_df.columns = ["_".join(col).strip() for col in summary_df.columns.values] +summary_df.rename(columns={"country_": "country"}, inplace=True) + +# Set up the UI + +ui.page_opts(fillable=True) + +ui.markdown( + "**Instructions**: Select one or more countries in the table below to see more information." +) + +with ui.layout_columns(col_widths=[12, 6, 6]): + with ui.card(height="400px"): + + @render.data_frame + def summary_data(): + return render.DataGrid(summary_df.round(2), row_selection_mode="multiple") + + with ui.card(height="400px"): + + @render_widget + def country_detail_pop(): + return px.line( + filtered_df(), + x="year", + y="pop", + color="country", + title="Population Over Time", + ) + + with ui.card(height="400px"): + + @render_widget + def country_detail_percap(): + return px.line( + filtered_df(), + x="year", + y="gdpPercap", + color="country", + title="GDP per Capita Over Time", + ) + + +@reactive.calc +def filtered_df(): + # input.summary_data_selected_rows() is a tuple, so we must convert it to list, + # as that's what Pandas requires for indexing. + selected_idx = list(req(input.summary_data_selected_rows())) + countries = summary_df.iloc[selected_idx]["country"] + # Filter data for selected countries + return df[df["country"].isin(countries)] diff --git a/shiny/api-examples/download/app-express.py b/shiny/api-examples/download/app-express.py new file mode 100644 index 000000000..748dce7c4 --- /dev/null +++ b/shiny/api-examples/download/app-express.py @@ -0,0 +1,76 @@ +import asyncio +import io +import os +from datetime import date + +import matplotlib.pyplot as plt +import numpy as np + +from shiny.express import render, ui + +ui.page_opts(title="Various download examples") + +with ui.accordion(open=True): + with ui.accordion_panel("Simple case"): + ui.markdown("Downloads a pre-existing file, using its existing name on disk.") + + @render.download(label="Download CSV") + 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 + + with ui.accordion_panel("Dynamic data generation"): + ui.markdown("Downloads a PNG that's generated on the fly.") + + ui.input_text("title", "Plot title", "Random scatter plot") + ui.input_slider("num_points", "Number of data points", min=1, max=100, value=50) + + @render.download(label="Download plot", 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 @render.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() + + with ui.accordion_panel("Dynamic filename"): + ui.markdown( + "Demonstrates that filenames can be generated on the fly (and use Unicode characters!)." + ) + + @render.download( + label="Download filename", + 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" + + with ui.accordion_panel("Failed downloads"): + ui.markdown( + "Throws an error in the download handler, download should not succeed." + ) + + @render.download(label="Download", filename="failuretest.txt") + async def download4(): + yield "hello" + raise Exception("This error was caused intentionally") diff --git a/shiny/api-examples/dynamic_route/app-express.py b/shiny/api-examples/dynamic_route/app-express.py new file mode 100644 index 000000000..0ac6074b1 --- /dev/null +++ b/shiny/api-examples/dynamic_route/app-express.py @@ -0,0 +1,31 @@ +from starlette.requests import Request +from starlette.responses import JSONResponse + +from shiny import reactive +from shiny.express import input, session, ui + +ui.input_action_button("serve", "Click to serve") + +ui.div(id="messages") + + +@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", + ) diff --git a/shiny/api-examples/event/app-express.py b/shiny/api-examples/event/app-express.py new file mode 100644 index 000000000..4d47e06d5 --- /dev/null +++ b/shiny/api-examples/event/app-express.py @@ -0,0 +1,74 @@ +import random + +from shiny import reactive +from shiny.express import input, render, ui +from shiny.ui import output_ui + +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 {output_ui("number", inline=True)}), but + the output is only updated when the button is clicked. + """ +) + +# Always update this output when the number is updated +with ui.hold(): + + @render.ui + def number(): + return val.get() + + +ui.input_action_button("btn_out", "(1) Update number") + + +# 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.) +@render.text +@reactive.event(input.btn_out, ignore_none=False) +def out_out(): + return str(val.get()) + + +ui.input_action_button("btn_calc", "(2) Show 1 / number") + + +@render.text +def out_calc(): + return str(calc()) + + +ui.input_action_button("btn_effect", "(3) Log number") +ui.div(id="out_effect") + + +# 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)) + + +@reactive.calc +@reactive.event(input.btn_calc) +def calc(): + return 1 / val.get() + + +@reactive.effect +@reactive.event(input.btn_effect) +def _(): + ui.insert_ui( + ui.p("Random number! ", val.get()), + selector="#out_effect", + where="afterEnd", + ) diff --git a/shiny/api-examples/extended_task/app-core.py b/shiny/api-examples/extended_task/app-core.py index 6fa9cbe8b..aa5ef637e 100644 --- a/shiny/api-examples/extended_task/app-core.py +++ b/shiny/api-examples/extended_task/app-core.py @@ -1,49 +1,52 @@ import asyncio from datetime import datetime -from shiny import reactive, render -from shiny.express import input, ui - -ui.h5("Current time") - - -@render.text -def current_time(): - reactive.invalidate_later(1) - return datetime.now().strftime("%H:%M:%S") - - -with ui.p(): - "Notice that the time above updates every second, even if you click the button below." - - -@ui.bind_task_button(button_id="btn") -@reactive.extended_task -async def slow_compute(a: int, b: int) -> int: - await asyncio.sleep(3) - return a + b - +from shiny import App, reactive, render, ui + +app_ui = ui.page_fixed( + ui.h5("Current time"), + ui.output_text("current_time"), + ui.p( + "Notice that the time above updates every second, even if you click the button below." + ), + ui.layout_sidebar( + ui.sidebar( + ui.input_numeric("x", "x", 1), + ui.input_numeric("y", "y", 2), + ui.input_task_button("btn", "Compute, slowly"), + ui.input_action_button("btn_cancel", "Cancel"), + ), + ui.h5("Sum of x and y"), + ui.output_text("show_result"), + ), +) + + +def server(input, output, session): + @render.text + def current_time(): + reactive.invalidate_later(1) + return datetime.now().strftime("%H:%M:%S") -with ui.layout_sidebar(): - with ui.sidebar(): - ui.input_numeric("x", "x", 1) - ui.input_numeric("y", "y", 2) - ui.input_task_button("btn", "Compute, slowly") - ui.input_action_button("btn_cancel", "Cancel") + @ui.bind_task_button(button_id="btn") + @reactive.extended_task + async def slow_compute(a: int, b: int) -> int: + await asyncio.sleep(3) + return a + b - @reactive.Effect + @reactive.effect @reactive.event(input.btn, ignore_none=False) def handle_click(): - # slow_compute.cancel() slow_compute(input.x(), input.y()) - @reactive.Effect + @reactive.effect @reactive.event(input.btn_cancel) def handle_cancel(): slow_compute.cancel() - ui.h5("Sum of x and y") - @render.text def show_result(): return str(slow_compute.result()) + + +app = App(app_ui, server) diff --git a/shiny/api-examples/extended_task/app-express.py b/shiny/api-examples/extended_task/app-express.py new file mode 100644 index 000000000..6fa9cbe8b --- /dev/null +++ b/shiny/api-examples/extended_task/app-express.py @@ -0,0 +1,49 @@ +import asyncio +from datetime import datetime + +from shiny import reactive, render +from shiny.express import input, ui + +ui.h5("Current time") + + +@render.text +def current_time(): + reactive.invalidate_later(1) + return datetime.now().strftime("%H:%M:%S") + + +with ui.p(): + "Notice that the time above updates every second, even if you click the button below." + + +@ui.bind_task_button(button_id="btn") +@reactive.extended_task +async def slow_compute(a: int, b: int) -> int: + await asyncio.sleep(3) + return a + b + + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_numeric("x", "x", 1) + ui.input_numeric("y", "y", 2) + ui.input_task_button("btn", "Compute, slowly") + ui.input_action_button("btn_cancel", "Cancel") + + @reactive.Effect + @reactive.event(input.btn, ignore_none=False) + def handle_click(): + # slow_compute.cancel() + slow_compute(input.x(), input.y()) + + @reactive.Effect + @reactive.event(input.btn_cancel) + def handle_cancel(): + slow_compute.cancel() + + ui.h5("Sum of x and y") + + @render.text + def show_result(): + return str(slow_compute.result()) diff --git a/shiny/api-examples/file_reader/app-express.py b/shiny/api-examples/file_reader/app-express.py new file mode 100644 index 000000000..0f2491da1 --- /dev/null +++ b/shiny/api-examples/file_reader/app-express.py @@ -0,0 +1,18 @@ +import pathlib + +import pandas as pd + +from shiny import reactive +from shiny.express import render + +file = pathlib.Path(__file__).parent / "mtcars.csv" + + +@reactive.file_reader(file) +def read_file(): + return pd.read_csv(file) + + +@render.table +def result(): + return read_file() diff --git a/shiny/api-examples/include_css/app-express.py b/shiny/api-examples/include_css/app-express.py new file mode 100644 index 000000000..fd3ef27ec --- /dev/null +++ b/shiny/api-examples/include_css/app-express.py @@ -0,0 +1,12 @@ +from pathlib import Path + +from shiny.express import ui + +css_file = Path(__file__).parent / "css" / "styles.css" + +"Almost before we knew it, we had left the ground!!!" + +ui.include_css(css_file) + +# Style individual elements with an attribute dictionary. +ui.p("Bold text", {"style": "font-weight: bold"}) diff --git a/shiny/api-examples/include_js/app-express.py b/shiny/api-examples/include_js/app-express.py new file mode 100644 index 000000000..21459e401 --- /dev/null +++ b/shiny/api-examples/include_js/app-express.py @@ -0,0 +1,9 @@ +from pathlib import Path + +from shiny.express import ui + +js_file = Path(__file__).parent / "js" / "app.js" + +"If you see this page before 'OK'-ing the alert box, something went wrong" + +ui.include_js(js_file) diff --git a/shiny/api-examples/insert_accordion_panel/app-express.py b/shiny/api-examples/insert_accordion_panel/app-express.py new file mode 100644 index 000000000..1de832962 --- /dev/null +++ b/shiny/api-examples/insert_accordion_panel/app-express.py @@ -0,0 +1,20 @@ +import random + +from shiny import reactive, ui +from shiny.express import input + + +def make_panel(letter): + return ui.accordion_panel( + f"Section {letter}", f"Some narrative for section {letter}" + ) + + +ui.input_action_button("add_panel", "Add random panel", class_="mt-3 mb-3") +ui.accordion(*[make_panel(letter) for letter in "ABCDE"], id="acc", multiple=True) + + +@reactive.effect +@reactive.event(input.add_panel) +def _(): + ui.insert_accordion_panel("acc", make_panel(str(random.randint(0, 10000)))) diff --git a/shiny/api-examples/insert_ui/app-express.py b/shiny/api-examples/insert_ui/app-express.py new file mode 100644 index 000000000..37a0b72f9 --- /dev/null +++ b/shiny/api-examples/insert_ui/app-express.py @@ -0,0 +1,14 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("add", "Add UI") + + +@reactive.Effect +@reactive.event(input.add) +def _(): + ui.insert_ui( + ui.input_text("txt" + str(input.add()), "Enter some text"), + selector="#add", + where="afterEnd", + ) diff --git a/shiny/api-examples/modal/app-express.py b/shiny/api-examples/modal/app-express.py new file mode 100644 index 000000000..6a1382378 --- /dev/null +++ b/shiny/api-examples/modal/app-express.py @@ -0,0 +1,16 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("show", "Show modal dialog") + + +@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) diff --git a/shiny/api-examples/nav_panel/app-express.py b/shiny/api-examples/nav_panel/app-express.py new file mode 100644 index 000000000..a5d57a81b --- /dev/null +++ b/shiny/api-examples/nav_panel/app-express.py @@ -0,0 +1,26 @@ +from shiny.express import ui + +ui.page_opts(title="Nav Panel Example") + +with ui.nav_panel("Page 1"): + "Page 1 content" + +with ui.nav_panel("Page 2"): + with ui.navset_card_underline(): + with ui.nav_panel("Tab 1"): + "Tab 1 content" + with ui.nav_panel("Tab 2"): + "Tab 2 content" + with ui.nav_panel("Tab 3"): + "Tab 3 content" + +ui.nav_spacer() + +with ui.nav_menu("Links", align="right"): + with ui.nav_control(): + ui.a("Shiny", href="https://shiny.posit.co/py/", target="_blank") + "----" + "Plain text" + "----" + with ui.nav_control(): + ui.a("Posit", href="https://posit.co", target="_blank") diff --git a/shiny/api-examples/navset_hidden/app-core.py b/shiny/api-examples/navset_hidden/app-core.py index 014869a97..c1da6c78d 100644 --- a/shiny/api-examples/navset_hidden/app-core.py +++ b/shiny/api-examples/navset_hidden/app-core.py @@ -1,21 +1,17 @@ from shiny import App, Inputs, Outputs, Session, reactive, ui -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_panel(None, "Panel 1 content", value="panel1"), - ui.nav_panel(None, "Panel 2 content", value="panel2"), - ui.nav_panel(None, "Panel 3 content", value="panel3"), - id="hidden_tabs", - ), - ), - ) +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_radio_buttons( + "controller", "Controller", ["1", "2", "3"], selected="1" + ) + ), + ui.navset_hidden( + ui.nav_panel(None, "Panel 1 content", value="panel1"), + ui.nav_panel(None, "Panel 2 content", value="panel2"), + ui.nav_panel(None, "Panel 3 content", value="panel3"), + id="hidden_tabs", + ), ) diff --git a/shiny/api-examples/navset_hidden/app-express.py b/shiny/api-examples/navset_hidden/app-express.py new file mode 100644 index 000000000..7072f15f5 --- /dev/null +++ b/shiny/api-examples/navset_hidden/app-express.py @@ -0,0 +1,19 @@ +from shiny import reactive +from shiny.express import input, ui + +with ui.sidebar(): + ui.input_radio_buttons("controller", "Controller", ["1", "2", "3"], selected="1") + +with ui.navset_hidden(id="hidden_tabs"): + with ui.nav_panel(None, value="panel1"): + "Panel 1 content" + with ui.nav_panel(None, value="panel2"): + "Panel 2 content" + with ui.nav_panel(None, value="panel3"): + "Panel 3 content" + + +@reactive.effect +@reactive.event(input.controller) +def _(): + ui.update_navs("hidden_tabs", selected="panel" + str(input.controller())) diff --git a/shiny/api-examples/notification_show/app-express.py b/shiny/api-examples/notification_show/app-express.py new file mode 100644 index 000000000..d64a57431 --- /dev/null +++ b/shiny/api-examples/notification_show/app-express.py @@ -0,0 +1,27 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("show", "Show") +ui.input_action_button("remove", "Remove") + +ids: list[str] = [] +n: int = 0 + + +@reactive.effect +@reactive.event(input.show) +def _(): + global ids + global 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 _(): + global ids + if ids: + ui.notification_remove(ids.pop()) diff --git a/shiny/api-examples/on_ended/app-express.py b/shiny/api-examples/on_ended/app-express.py new file mode 100644 index 000000000..6e42f89da --- /dev/null +++ b/shiny/api-examples/on_ended/app-express.py @@ -0,0 +1,19 @@ +from datetime import datetime + +from shiny import reactive +from shiny.express import input, session, ui + +ui.input_action_button("close", "Close the 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() diff --git a/shiny/api-examples/on_flush/app-express.py b/shiny/api-examples/on_flush/app-express.py new file mode 100644 index 000000000..6c765c96e --- /dev/null +++ b/shiny/api-examples/on_flush/app-express.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from shiny.express import input, render, session, ui + +ui.input_action_button("flush", "Trigger flush") + + +@render.ui +def n_clicks(): + return "Number of clicks: " + str(input.flush()) + + +ui.div(id="flush_time") + + +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", + ) + + +if hasattr(session, "on_flush"): + _ = session.on_flush(log, once=False) diff --git a/shiny/api-examples/on_flushed/app-express.py b/shiny/api-examples/on_flushed/app-express.py new file mode 100644 index 000000000..c6f77cceb --- /dev/null +++ b/shiny/api-examples/on_flushed/app-express.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from shiny.express import input, render, session, ui + +ui.input_action_button("flush", "Trigger flush") + + +@render.ui +def n_clicks(): + return "Number of clicks: " + str(input.flush()) + + +ui.div(id="flush_time") + + +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", + ) + + +if hasattr(session, "on_flushed"): + _ = session.on_flushed(log, once=False) diff --git a/shiny/api-examples/output_image/app-express.py b/shiny/api-examples/output_image/app-express.py new file mode 100644 index 000000000..be285d774 --- /dev/null +++ b/shiny/api-examples/output_image/app-express.py @@ -0,0 +1,10 @@ +from shiny.express import render + + +@render.image +def image(): + from pathlib import Path + + dir = Path(__file__).resolve().parent + img = {"src": str(dir / "posit-logo.png"), "width": "100px"} + return img diff --git a/shiny/api-examples/output_plot/app-express.py b/shiny/api-examples/output_plot/app-express.py new file mode 100644 index 000000000..f328cb6eb --- /dev/null +++ b/shiny/api-examples/output_plot/app-express.py @@ -0,0 +1,15 @@ +import matplotlib.pyplot as plt +import numpy as np + +from shiny.express import input, render, ui + +ui.input_slider("n", "input_slider()", min=10, max=100, value=50, step=5, animate=True) + + +@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 diff --git a/shiny/api-examples/output_table/app-express.py b/shiny/api-examples/output_table/app-express.py new file mode 100644 index 000000000..12d8cb8aa --- /dev/null +++ b/shiny/api-examples/output_table/app-express.py @@ -0,0 +1,49 @@ +import pathlib + +import pandas as pd + +from shiny.express import input, render, ui + +dir = pathlib.Path(__file__).parent +mtcars = pd.read_csv(dir / "mtcars.csv") + + +ui.input_checkbox("highlight", "Highlight min/max values") + + +@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") + ) + + +# Legend +with ui.panel_conditional("input.highlight"): + with ui.panel_absolute(bottom="6px", right="6px", class_="p-1 bg-light border"): + "Yellow is maximum, grey is minimum" diff --git a/shiny/api-examples/output_text/app-express.py b/shiny/api-examples/output_text/app-express.py new file mode 100644 index 000000000..c5ad6f6da --- /dev/null +++ b/shiny/api-examples/output_text/app-express.py @@ -0,0 +1,28 @@ +from shiny.express import input, output_args, render, ui + +ui.input_text("txt", "Enter the text to display below:", "delete me") + +with ui.card(): + ui.card_header(ui.code("ui.render_text")) + + @render.text + def text1(): + return input.txt() + + +with ui.card(): + ui.card_header(ui.code("@output_args(placeholder=True)")) + + @output_args(placeholder=True) + @render.code + def text2(): + return input.txt() + + +with ui.card(): + ui.card_header(ui.code("@output_args(placeholder=False)")) + + @output_args(placeholder=False) + @render.code + def text3(): + return input.txt() diff --git a/shiny/api-examples/output_ui/app-express.py b/shiny/api-examples/output_ui/app-express.py new file mode 100644 index 000000000..fb6c99294 --- /dev/null +++ b/shiny/api-examples/output_ui/app-express.py @@ -0,0 +1,13 @@ +from shiny import reactive +from shiny.express import input, render, ui + +ui.input_action_button("add", "Add more controls") + + +@render.ui +@reactive.event(input.add) +def moreControls(): + return [ + ui.input_slider("n", "N", min=1, max=1000, value=500), + ui.input_text("label", "Label"), + ] diff --git a/shiny/api-examples/page_fixed/app-express.py b/shiny/api-examples/page_fixed/app-express.py new file mode 100644 index 000000000..91f751edd --- /dev/null +++ b/shiny/api-examples/page_fixed/app-express.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +from shiny.express import input, render, ui + +# Equivalent to using `ui.page_fixed()` in the core API +ui.page_opts(full_width=False) + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_slider("n", "N", min=0, max=100, value=20) + + @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 diff --git a/shiny/api-examples/page_fluid/app-core.py b/shiny/api-examples/page_fluid/app-core.py index 99a7ef9f8..a05d3618a 100644 --- a/shiny/api-examples/page_fluid/app-core.py +++ b/shiny/api-examples/page_fluid/app-core.py @@ -5,13 +5,11 @@ app_ui = ui.page_fluid( ui.layout_sidebar( - ui.panel_sidebar( + ui.sidebar( ui.input_slider("n", "N", min=0, max=100, value=20), ), - ui.panel_main( - ui.output_plot("plot"), - ), - ), + ui.output_plot("plot"), + ) ) diff --git a/shiny/api-examples/page_fluid/app-express.py b/shiny/api-examples/page_fluid/app-express.py new file mode 100644 index 000000000..587ec743a --- /dev/null +++ b/shiny/api-examples/page_fluid/app-express.py @@ -0,0 +1,20 @@ +import matplotlib.pyplot as plt +import numpy as np + +from shiny.express import input, render, ui + +# Equivalent to using `ui.page_fluid()` in the core API +ui.page_opts(full_width=True) + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_slider("n", "N", min=0, max=100, value=20) + + @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 diff --git a/shiny/api-examples/panel_title/app-express.py b/shiny/api-examples/panel_title/app-express.py new file mode 100644 index 000000000..2d7eaad4c --- /dev/null +++ b/shiny/api-examples/panel_title/app-express.py @@ -0,0 +1,3 @@ +from shiny.express import ui + +ui.panel_title("Page title", "Window title") diff --git a/shiny/api-examples/panel_title/app.py b/shiny/api-examples/panel_title/app.py new file mode 100644 index 000000000..7b42ac5d6 --- /dev/null +++ b/shiny/api-examples/panel_title/app.py @@ -0,0 +1,10 @@ +from shiny import App, Inputs, Outputs, Session, ui + +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) diff --git a/shiny/api-examples/poll/app-express.py b/shiny/api-examples/poll/app-express.py new file mode 100644 index 000000000..df1f9c2cd --- /dev/null +++ b/shiny/api-examples/poll/app-express.py @@ -0,0 +1,106 @@ +import asyncio +import random +import sqlite3 +from datetime import datetime +from typing import Any, Awaitable + +import pandas as pd + +from shiny import reactive +from shiny.express import input, render, ui + +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) + + +with ui.card(): + 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`. + """ + ) + ui.input_selectize("symbols", "Filter by symbol", [""] + SYMBOLS, multiple=True) + + @render.data_frame + def table(): + df = stock_quotes() + if input.symbols(): + df = df[df["symbol"].isin(input.symbols())] + return df diff --git a/shiny/api-examples/popover/app-express.py b/shiny/api-examples/popover/app-express.py new file mode 100644 index 000000000..6056197fa --- /dev/null +++ b/shiny/api-examples/popover/app-express.py @@ -0,0 +1,23 @@ +from icons import gear_fill + +from shiny.express import input, render, ui + +with ui.popover(id="btn_popover"): + ui.input_action_button("btn", "A button", class_="mt-3") + + "A popover with more context and information than should be used in a tooltip." + "You can even have multiple DOM elements in a popover!" + + +with ui.card(class_="mt-3"): + with ui.card_header(): + "Plot title (Click the gear to change variables)" + with ui.popover(placement="right", id="card_popover"): + ui.span(gear_fill, style="position:absolute; top: 5px; right: 7px;") + "Put dropdowns here to alter your plot!" + ui.input_selectize("x", "X", ["x1", "x2", "x3"]) + ui.input_selectize("y", "Y", ["y1", "y2", "y3"]) + + @render.text + def plot_txt(): + return f"" diff --git a/shiny/api-examples/remove_accordion_panel/app-express.py b/shiny/api-examples/remove_accordion_panel/app-express.py new file mode 100644 index 000000000..04c9a087a --- /dev/null +++ b/shiny/api-examples/remove_accordion_panel/app-express.py @@ -0,0 +1,38 @@ +import random + +from shiny import reactive +from shiny.express import input, ui + +choices = ["A", "B", "C", "D", "E"] +random.shuffle(choices) + +ui.input_action_button( + "remove_panel", + f"Remove Section {choices[-1]}", + class_="mt-3 mb-3", +) + +" (Sections randomly picked at server start)" + +with ui.accordion(id="acc", multiple=True): + for letter in "ABCDE": + with ui.accordion_panel(f"Section {letter}"): + f"Some narrative for section {letter}" + + +user_choices = [choice for choice in choices] + + +@reactive.effect +@reactive.event(input.remove_panel) +def _(): + if len(user_choices) == 0: + ui.notification_show("No more panels to remove!") + return + + ui.remove_accordion_panel("acc", f"Section { user_choices.pop() }") + + label = "No more panels to remove!" + if len(user_choices) > 0: + label = f"Remove Section {user_choices[-1]}" + ui.update_action_button("remove_panel", label=label) diff --git a/shiny/api-examples/remove_ui/app-express.py b/shiny/api-examples/remove_ui/app-express.py new file mode 100644 index 000000000..b491abe4b --- /dev/null +++ b/shiny/api-examples/remove_ui/app-express.py @@ -0,0 +1,11 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("rmv", "Remove UI") +ui.input_text("txt", "Click button above to remove me") + + +@reactive.Effect +@reactive.event(input.rmv) +def _(): + ui.remove_ui(selector="div:has(> #txt)") diff --git a/shiny/api-examples/req/app-express.py b/shiny/api-examples/req/app-express.py new file mode 100644 index 000000000..7523dba2f --- /dev/null +++ b/shiny/api-examples/req/app-express.py @@ -0,0 +1,46 @@ +from shiny import reactive, req +from shiny.express import input, render, ui +from shiny.types import SafeException + +ui.input_action_button("safe", "Throw a safe error") + + +@render.ui +def safe(): + # This error _won't_ be sanitized when deployed (i.e., it's "safe") + raise SafeException(f"You've clicked {str(safe_click())} times") + + +ui.input_action_button("unsafe", "Throw an unsafe error") + + +@render.ui +def unsafe(): + req(input.unsafe()) + # This error _will_ be sanitized when deployed (i.e., it's "unsafe") + raise Exception(f"Super secret number of clicks: {str(input.unsafe())}") + + +ui.input_text( + "txt", + "Enter some text below, then remove it. Notice how the text is never fully removed.", +) + + +@render.ui +def txt_out(): + req(input.txt(), cancel_output=True) + return input.txt() + + +@reactive.calc +def safe_click(): + req(input.safe()) + return input.safe() + + +@reactive.effect +def _(): + req(input.unsafe()) + print("unsafe clicks:", input.unsafe()) + # raise Exception("Observer exception: this should cause a crash") diff --git a/shiny/api-examples/send_custom_message/app-express.py b/shiny/api-examples/send_custom_message/app-express.py new file mode 100644 index 000000000..8ade5c6d5 --- /dev/null +++ b/shiny/api-examples/send_custom_message/app-express.py @@ -0,0 +1,23 @@ +from shiny import reactive +from shiny.express import input, session, ui + +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) { + $("

").text(message.msg).appendTo("#messages"); + }); + }); + """ +) + + +@reactive.effect +@reactive.event(input.submit) +async def _(): + await session.send_custom_message("append_msg", {"msg": input.msg()}) diff --git a/shiny/api-examples/tooltip/app-express.py b/shiny/api-examples/tooltip/app-express.py new file mode 100644 index 000000000..0d099e50c --- /dev/null +++ b/shiny/api-examples/tooltip/app-express.py @@ -0,0 +1,16 @@ +from icons import question_circle_fill + +from shiny.express import ui + +with ui.tooltip(id="btn_tooltip"): + ui.input_action_button("btn", "A button", class_="mt-3") + + "A message" + +with ui.card(class_="mt-3"): + with ui.card_header(): + with ui.tooltip(placement="right", id="card_tooltip"): + ui.span("Card title ", question_circle_fill) + "Additional info" + + "Card body content..." diff --git a/shiny/api-examples/update_accordion/app-express.py b/shiny/api-examples/update_accordion/app-express.py new file mode 100644 index 000000000..c16cd6ade --- /dev/null +++ b/shiny/api-examples/update_accordion/app-express.py @@ -0,0 +1,15 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("set_acc", "Only open sections A,C,E", class_="mt-3 mb-3") + +with ui.accordion(id="acc", open=["Section B", "Section D"], multiple=True): + for letter in "ABCDE": + with ui.accordion_panel(f"Section {letter}"): + f"Some narrative for section {letter}" + + +@reactive.effect +@reactive.event(input.set_acc) +def _(): + ui.update_accordion("acc", show=["Section A", "Section C", "Section E"]) diff --git a/shiny/api-examples/update_accordion_panel/app-express.py b/shiny/api-examples/update_accordion_panel/app-express.py new file mode 100644 index 000000000..f6dbba032 --- /dev/null +++ b/shiny/api-examples/update_accordion_panel/app-express.py @@ -0,0 +1,28 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_switch("update_panel", "Update (and open) Sections") + +with ui.accordion(id="acc", multiple=True): + for letter in "ABCDE": + with ui.accordion_panel(f"Section {letter}", value=f"sec_{letter}"): + f"Some narrative for section {letter}" + + +@reactive.effect +@reactive.event(input.update_panel) +def _(): + txt = " (updated)" if input.update_panel() else "" + show = bool(input.update_panel() % 2 == 1) + for letter in "ABCDE": + ui.update_accordion_panel( + "acc", + f"sec_{letter}", + f"Some{txt} narrative for section {letter}", + title=f"Section {letter}{txt}", + # Open Accordion Panel to see updated contents + show=show, + ) + next_show_txt = "close" if show else "open" + + ui.update_switch("update_panel", label=f"Update (and {next_show_txt}) Sections") diff --git a/shiny/api-examples/update_action_button/app-express.py b/shiny/api-examples/update_action_button/app-express.py new file mode 100644 index 000000000..d93b21ca6 --- /dev/null +++ b/shiny/api-examples/update_action_button/app-express.py @@ -0,0 +1,24 @@ +from shiny import reactive, req +from shiny.express import input, ui + +with ui.sidebar(): + ui.input_action_button("update", "Update other buttons and link") + +with ui.layout_column_wrap(): + ui.input_action_button("goButton", "Go") + ui.input_action_button("goButton2", "Go 2", icon="🤩") + ui.input_action_button("goButton3", "Go 3") + ui.input_action_link("goLink", "Go Link") + + +@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="🔗") diff --git a/shiny/api-examples/update_checkbox/app-express.py b/shiny/api-examples/update_checkbox/app-express.py new file mode 100644 index 000000000..3d79840cd --- /dev/null +++ b/shiny/api-examples/update_checkbox/app-express.py @@ -0,0 +1,12 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_slider("controller", "Controller", min=0, max=1, value=0, step=1) +ui.input_checkbox("inCheckbox", "Input checkbox") + + +@reactive.effect +def _(): + # True if controller is odd, False if even. + x_even = input.controller() % 2 == 1 + ui.update_checkbox("inCheckbox", value=x_even) diff --git a/shiny/api-examples/update_checkbox_group/app-express.py b/shiny/api-examples/update_checkbox_group/app-express.py new file mode 100644 index 000000000..d25764e99 --- /dev/null +++ b/shiny/api-examples/update_checkbox_group/app-express.py @@ -0,0 +1,23 @@ +from shiny import reactive +from shiny.express import input, ui + +"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"] +) + + +@reactive.effect +def _(): + x = input.inCheckboxGroup() + + # Can also set the label and select items + ui.update_checkbox_group( + "inCheckboxGroup2", + label="Checkboxgroup label " + str(len(x)), + choices=x, + selected=x, + ) diff --git a/shiny/api-examples/update_date/app-express.py b/shiny/api-examples/update_date/app-express.py new file mode 100644 index 000000000..cc83e2788 --- /dev/null +++ b/shiny/api-examples/update_date/app-express.py @@ -0,0 +1,19 @@ +from datetime import date, timedelta + +from shiny import reactive +from shiny.express import input, ui + +ui.input_slider("n", "Day of month", min=1, max=30, value=10) +ui.input_date("inDate", "Input date") + + +@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), + ) diff --git a/shiny/api-examples/update_date_range/app-express.py b/shiny/api-examples/update_date_range/app-express.py new file mode 100644 index 000000000..d57a9efff --- /dev/null +++ b/shiny/api-examples/update_date_range/app-express.py @@ -0,0 +1,20 @@ +from datetime import date, timedelta + +from shiny import reactive +from shiny.express import input, ui + +ui.input_slider("n", "Day of month", min=1, max=30, value=10) +ui.input_date_range("inDateRange", "Input date") + + +@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), + ) diff --git a/shiny/api-examples/update_navs/app-core.py b/shiny/api-examples/update_navs/app-core.py index 6a8e392a9..c2d807d0d 100644 --- a/shiny/api-examples/update_navs/app-core.py +++ b/shiny/api-examples/update_navs/app-core.py @@ -1,19 +1,13 @@ from shiny import App, Inputs, Outputs, Session, reactive, ui -app_ui = ui.page_fixed( - ui.layout_sidebar( - ui.panel_sidebar( - ui.input_slider("controller", "Controller", min=1, max=3, value=1) - ), - ui.panel_main( - ui.navset_card_tab( - ui.nav_panel("Panel 1", "Panel 1 content", value="panel1"), - ui.nav_panel("Panel 2", "Panel 2 content", value="panel2"), - ui.nav_panel("Panel 3", "Panel 3 content", value="panel3"), - id="inTabset", - ), - ), - ) +app_ui = ui.page_sidebar( + ui.sidebar(ui.input_slider("controller", "Controller", min=1, max=3, value=1)), + ui.navset_card_tab( + ui.nav_panel("Panel 1", "Panel 1 content", value="panel1"), + ui.nav_panel("Panel 2", "Panel 2 content", value="panel2"), + ui.nav_panel("Panel 3", "Panel 3 content", value="panel3"), + id="inTabset", + ), ) diff --git a/shiny/api-examples/update_navs/app-express.py b/shiny/api-examples/update_navs/app-express.py new file mode 100644 index 000000000..2331717a4 --- /dev/null +++ b/shiny/api-examples/update_navs/app-express.py @@ -0,0 +1,18 @@ +from shiny import reactive +from shiny.express import input, ui + +with ui.sidebar(): + ui.input_slider("controller", "Controller", min=1, max=3, value=1) + +with ui.navset_card_tab(id="inTabset"): + with ui.nav_panel("Panel 1", value="panel1"): + "Panel 1 content" + with ui.nav_panel("Panel 2", value="panel2"): + "Panel 2 content" + with ui.nav_panel("Panel 3", value="panel3"): + "Panel 3 content" + + +@reactive.effect +def _(): + ui.update_navs("inTabset", selected="panel" + str(input.controller())) diff --git a/shiny/api-examples/update_numeric/app-express.py b/shiny/api-examples/update_numeric/app-express.py new file mode 100644 index 000000000..8f3071617 --- /dev/null +++ b/shiny/api-examples/update_numeric/app-express.py @@ -0,0 +1,20 @@ +from shiny import reactive +from shiny.express import input, ui + +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) + + +@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, + ) diff --git a/shiny/api-examples/update_popover/app-core.py b/shiny/api-examples/update_popover/app-core.py index f54f18de9..9c6542ba0 100644 --- a/shiny/api-examples/update_popover/app-core.py +++ b/shiny/api-examples/update_popover/app-core.py @@ -1,4 +1,4 @@ -from shiny import App, Inputs, Outputs, Session, reactive, req, ui +from shiny import App, Inputs, Outputs, Session, reactive, ui app_ui = ui.page_fluid( ui.input_action_button("btn_show", "Show popover", class_="mt-3 me-3"), @@ -14,21 +14,19 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect + @reactive.event(input.btn_show) def _(): - req(input.btn_show()) - ui.update_popover("popover_id", show=True) - @reactive.Effect + @reactive.effect + @reactive.event(input.btn_close) def _(): - req(input.btn_close()) - ui.update_popover("popover_id", show=False) - @reactive.Effect + @reactive.effect + @reactive.event(input.btn_w_popover) def _(): - req(input.btn_w_popover()) ui.notification_show("Button clicked!", duration=3, type="message") diff --git a/shiny/api-examples/update_popover/app-express.py b/shiny/api-examples/update_popover/app-express.py new file mode 100644 index 000000000..b8ec3afaf --- /dev/null +++ b/shiny/api-examples/update_popover/app-express.py @@ -0,0 +1,30 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("btn_show", "Show popover", class_="mt-3 me-3") +ui.input_action_button("btn_close", "Close popover", class_="mt-3 me-3") + +ui.br() +ui.br() + +with ui.popover(id="popover_id"): + ui.input_action_button("btn_w_popover", "A button w/ a popover", class_="mt-3") + "A message" + + +@reactive.effect +@reactive.event(input.btn_show) +def _(): + ui.update_popover("popover_id", show=True) + + +@reactive.effect +@reactive.event(input.btn_close) +def _(): + ui.update_popover("popover_id", show=False) + + +@reactive.effect +@reactive.event(input.btn_w_popover) +def _(): + ui.notification_show("Button clicked!", duration=3, type="message") diff --git a/shiny/api-examples/update_radio_buttons/app-express.py b/shiny/api-examples/update_radio_buttons/app-express.py new file mode 100644 index 000000000..6eebcc149 --- /dev/null +++ b/shiny/api-examples/update_radio_buttons/app-express.py @@ -0,0 +1,24 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.markdown("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"] +) + + +@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, + ) diff --git a/shiny/api-examples/update_select/app-express.py b/shiny/api-examples/update_select/app-express.py new file mode 100644 index 000000000..eaa0c57fa --- /dev/null +++ b/shiny/api-examples/update_select/app-express.py @@ -0,0 +1,27 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.markdown("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"]) + + +@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, + ) diff --git a/shiny/api-examples/update_selectize/app-express.py b/shiny/api-examples/update_selectize/app-express.py new file mode 100644 index 000000000..770d2edb0 --- /dev/null +++ b/shiny/api-examples/update_selectize/app-express.py @@ -0,0 +1,14 @@ +from shiny import reactive +from shiny.express import ui + +ui.input_selectize("x", "Server side selectize", choices=[], multiple=True) + + +@reactive.effect +def _(): + ui.update_selectize( + "x", + choices=[f"Foo {i}" for i in range(10000)], + selected=["Foo 0", "Foo 1"], + server=True, + ) diff --git a/shiny/api-examples/update_sidebar/app-express.py b/shiny/api-examples/update_sidebar/app-express.py new file mode 100644 index 000000000..b4101583a --- /dev/null +++ b/shiny/api-examples/update_sidebar/app-express.py @@ -0,0 +1,25 @@ +from shiny import reactive +from shiny.express import input, render, ui + +with ui.sidebar(id="sidebar"): + "Sidebar content" + +ui.input_action_button("open_sidebar", label="Open sidebar", class_="me-3") +ui.input_action_button("close_sidebar", label="Close sidebar", class_="me-3") + + +@render.text +def state(): + return f"input.sidebar(): {input.sidebar()}" + + +@reactive.effect +@reactive.event(input.open_sidebar) +def _(): + ui.update_sidebar("sidebar", show=True) + + +@reactive.effect +@reactive.event(input.close_sidebar) +def _(): + ui.update_sidebar("sidebar", show=False) diff --git a/shiny/api-examples/update_slider/app-core.py b/shiny/api-examples/update_slider/app-core.py index 923d12dcc..cbc0ce142 100644 --- a/shiny/api-examples/update_slider/app-core.py +++ b/shiny/api-examples/update_slider/app-core.py @@ -1,25 +1,29 @@ -from shiny import App, Inputs, Outputs, Session, reactive, ui +from shiny import App, reactive, ui app_ui = ui.page_fixed( - 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("Main app content"), - ) + ui.input_slider( + "receiver", "Receiver:", min=0, max=100, value=50, step=1, width="100%" + ), + ui.p( + "Change the min and max values below to see the receiver slider above update." + ), + ui.layout_column_wrap( + ui.input_slider("min", "Min:", min=0, max=50, value=0, step=1), + ui.input_slider("max", "Max:", min=50, max=100, value=100, step=1), + width=1 / 2, + ), ) -def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect +def server(input, output, 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. + # You can update the value, min, max, and step. ui.update_slider( - "receive", value=val, min=int(val / 2), max=val + 4, step=(val + 1) % 2 + 1 + "receiver", + value=max(min(input.receiver(), input.max()), input.min()), + min=input.min(), + max=input.max(), ) diff --git a/shiny/api-examples/update_slider/app-express.py b/shiny/api-examples/update_slider/app-express.py new file mode 100644 index 000000000..7b933cb7a --- /dev/null +++ b/shiny/api-examples/update_slider/app-express.py @@ -0,0 +1,20 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_slider("receiver", "Receiver:", min=0, max=100, value=50, step=1, width="100%") +ui.p("Change the min and max values below to see the receiver slider above update.") + +with ui.layout_column_wrap(width=1 / 2): + ui.input_slider("min", "Min:", min=0, max=50, value=0, step=1) + ui.input_slider("max", "Max:", min=50, max=100, value=100, step=1) + + +@reactive.effect +def _(): + # You can update the value, min, max, and step. + ui.update_slider( + "receiver", + value=max(min(input.receiver(), input.max()), input.min()), + min=input.min(), + max=input.max(), + ) diff --git a/shiny/api-examples/update_text/app-core.py b/shiny/api-examples/update_text/app-core.py index e7da63127..c200124e0 100644 --- a/shiny/api-examples/update_text/app-core.py +++ b/shiny/api-examples/update_text/app-core.py @@ -1,20 +1,30 @@ from shiny import App, Inputs, Outputs, Session, reactive, ui 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"), + ui.layout_column_wrap( + ui.input_radio_buttons( + "pet_type", "Pet type", ["Dog", "Cat", "Bird"], inline=True + ), + ui.input_radio_buttons("pet_sex", "Pet sex", ["Male", "Female"], inline=True), + ui.input_text("name", "Pet name", "Charlie"), + ui.input_text("royal_name", "Royal Name", "King Charlie"), + width=1 / 2, + ) ) def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect + @reactive.event(input.pet_type) 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) + # Update the label of the pet name input + ui.update_text("name", label=f"{input.pet_type()}'s name") + + @reactive.effect + def _(): + # Update the value of the royal name input + royal_noun = "King" if input.pet_sex() == "Male" else "Queen" + ui.update_text("royal_name", value=f"{royal_noun} {input.name()}") app = App(app_ui, server) diff --git a/shiny/api-examples/update_text/app-express.py b/shiny/api-examples/update_text/app-express.py new file mode 100644 index 000000000..a1ab5d2a8 --- /dev/null +++ b/shiny/api-examples/update_text/app-express.py @@ -0,0 +1,22 @@ +from shiny import reactive +from shiny.express import input, ui + +with ui.layout_column_wrap(width=1 / 2): + ui.input_radio_buttons("pet_type", "Pet type", ["Dog", "Cat", "Bird"], inline=True) + ui.input_radio_buttons("pet_sex", "Pet sex", ["Male", "Female"], inline=True) + ui.input_text("name", "Pet name", "Charlie") + ui.input_text("royal_name", "Royal Name", "King Charlie") + + +@reactive.effect +@reactive.event(input.pet_type) +def _(): + # Update the label of the pet name input + ui.update_text("name", label=f"{input.pet_type()}'s name") + + +@reactive.effect +def _(): + # Update the value of the royal name input + royal_noun = "King" if input.pet_sex() == "Male" else "Queen" + ui.update_text("royal_name", value=f"{royal_noun} {input.name()}") diff --git a/shiny/api-examples/update_tooltip/app-core.py b/shiny/api-examples/update_tooltip/app-core.py index aa81eee94..9d44842a3 100644 --- a/shiny/api-examples/update_tooltip/app-core.py +++ b/shiny/api-examples/update_tooltip/app-core.py @@ -1,16 +1,15 @@ -from shiny import App, Inputs, Outputs, Session, reactive, req, ui +from shiny import App, Inputs, Outputs, Session, reactive, ui app_ui = ui.page_fluid( ui.input_action_button("btn_show", "Show tooltip", class_="mt-3 me-3"), ui.input_action_button("btn_close", "Close tooltip", class_="mt-3 me-3"), - ui.br(), ui.input_action_button( "btn_update", "Update tooltip phrase (and show tooltip)", class_="mt-3 me-3" ), - ui.br(), - ui.br(), ui.tooltip( - ui.input_action_button("btn_w_tooltip", "A button w/ a tooltip", class_="mt-3"), + ui.input_action_button( + "btn_w_tooltip", "A button w/ a tooltip", class_="btn-primary mt-5" + ), "A message", id="tooltip_id", ), @@ -18,19 +17,17 @@ def server(input: Inputs, output: Outputs, session: Session): - @reactive.Effect + @reactive.effect + @reactive.event(input.btn_show) def _(): - req(input.btn_show()) - ui.update_tooltip("tooltip_id", show=True) - @reactive.Effect + @reactive.effect + @reactive.event(input.btn_close) def _(): - req(input.btn_close()) - ui.update_tooltip("tooltip_id", show=False) - @reactive.Effect + @reactive.effect @reactive.event(input.btn_update) def _(): content = ( @@ -39,9 +36,9 @@ def _(): ui.update_tooltip("tooltip_id", content, show=True) - @reactive.Effect + @reactive.effect + @reactive.event(input.btn_w_tooltip) def _(): - req(input.btn_w_tooltip()) ui.notification_show("Button clicked!", duration=3, type="message") diff --git a/shiny/api-examples/update_tooltip/app-express.py b/shiny/api-examples/update_tooltip/app-express.py new file mode 100644 index 000000000..235763ec5 --- /dev/null +++ b/shiny/api-examples/update_tooltip/app-express.py @@ -0,0 +1,42 @@ +from shiny import reactive +from shiny.express import input, ui + +ui.input_action_button("btn_show", "Show tooltip", class_="mt-3 me-3") +ui.input_action_button("btn_close", "Close tooltip", class_="mt-3 me-3") +ui.input_action_button( + "btn_update", "Update tooltip phrase (and show tooltip)", class_="mt-3 me-3" +) + +with ui.tooltip(id="tooltip_id"): + ui.input_action_button( + "btn_w_tooltip", + "A button w/ a tooltip", + class_="btn-primary mt-5", + ) + "A message" + + +@reactive.effect +@reactive.event(input.btn_show) +def _(): + ui.update_tooltip("tooltip_id", show=True) + + +@reactive.effect +@reactive.event(input.btn_close) +def _(): + ui.update_tooltip("tooltip_id", show=False) + + +@reactive.effect +@reactive.event(input.btn_update) +def _(): + content = "A " + " ".join(["NEW" for _ in range(input.btn_update())]) + " message" + + ui.update_tooltip("tooltip_id", content, show=True) + + +@reactive.effect +@reactive.event(input.btn_w_tooltip) +def _(): + ui.notification_show("Button clicked!", duration=3, type="message") diff --git a/shiny/api-examples/value_box/app-core.py b/shiny/api-examples/value_box/app-core.py index 1d5416ba7..9fe0a65a1 100644 --- a/shiny/api-examples/value_box/app-core.py +++ b/shiny/api-examples/value_box/app-core.py @@ -9,7 +9,7 @@ "$1 Billion Dollars", "Up 30% VS PREVIOUS 30 DAYS", showcase=piggy_bank, - theme="bg-gradient-orange-cyan", + theme="bg-gradient-orange-red", full_screen=True, ), ui.value_box( diff --git a/shiny/api-examples/value_box/app-express.py b/shiny/api-examples/value_box/app-express.py new file mode 100644 index 000000000..01f347dfe --- /dev/null +++ b/shiny/api-examples/value_box/app-express.py @@ -0,0 +1,28 @@ +from icons import piggy_bank + +from shiny.express import ui + +with ui.layout_columns(): + with ui.value_box( + showcase=piggy_bank, theme="bg-gradient-orange-red", full_screen=True + ): + "KPI Title" + "$1 Billion Dollars" + "Up 30% VS PREVIOUS 30 DAYS" + + with ui.value_box( + showcase=piggy_bank, + theme="text-green", + showcase_layout="top right", + full_screen=True, + ): + "KPI Title" + "$1 Billion Dollars" + "Up 30% VS PREVIOUS 30 DAYS" + + with ui.value_box( + showcase=piggy_bank, theme="purple", showcase_layout="bottom", full_screen=True + ): + "KPI Title" + "$1 Billion Dollars" + "Up 30% VS PREVIOUS 30 DAYS" diff --git a/shiny/express/_is_express.py b/shiny/express/_is_express.py index f6694a8ad..f2f1b643d 100644 --- a/shiny/express/_is_express.py +++ b/shiny/express/_is_express.py @@ -7,9 +7,12 @@ import ast from pathlib import Path +from .._docstring import no_example + __all__ = ("is_express_app",) +@no_example() def is_express_app(app: str, app_dir: str | None) -> bool: """Detect whether an app file is a Shiny express app diff --git a/shiny/express/_run.py b/shiny/express/_run.py index 860f2a67a..6b089e721 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -8,6 +8,7 @@ from htmltools import Tag, TagList from .._app import App +from .._docstring import no_example from ..session import Inputs, Outputs, Session, session_context from ._mock_session import MockSession from ._recall_context import RecallContextManager @@ -20,6 +21,7 @@ __all__ = ("wrap_express_app",) +@no_example() def wrap_express_app(file: Path) -> App: """Wrap a Shiny Express mode app into a Shiny `App` object. diff --git a/shiny/express/expressify_decorator/_expressify.py b/shiny/express/expressify_decorator/_expressify.py index 3f58434f1..163ab2f4e 100644 --- a/shiny/express/expressify_decorator/_expressify.py +++ b/shiny/express/expressify_decorator/_expressify.py @@ -17,6 +17,7 @@ runtime_checkable, ) +from ..._docstring import no_example from ..._shinyenv import is_pyodide from ._func_displayhook import _expressify_decorator_function_def from ._helpers import find_code_for_func @@ -101,6 +102,7 @@ def expressify() -> Callable[[TFunc], TFunc]: ... +@no_example() def expressify(fn: TFunc | None = None) -> TFunc | Callable[[TFunc], TFunc]: """ Decorate a function so that output is captured as in Shiny Express diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 579ab49bc..a665b7b9b 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -7,6 +7,7 @@ from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagFunction, TagList from ... import ui +from ..._docstring import add_example, no_example from ...types import MISSING, MISSING_TYPE from ...ui._accordion import AccordionPanel from ...ui._card import CardItem @@ -38,6 +39,7 @@ # ====================================================================================== # Shiny layout components # ====================================================================================== +@add_example() def sidebar( *, width: CssUnit = 250, @@ -127,6 +129,7 @@ def sidebar( # TODO: Figure out sidebar arg for ui.layout_sidebar +@add_example() def layout_sidebar( *, fillable: bool = True, @@ -204,6 +207,7 @@ def layout_sidebar( ) +@add_example() def layout_column_wrap( *, width: CssUnit | None | MISSING_TYPE = MISSING, @@ -286,6 +290,7 @@ def layout_column_wrap( ) +@add_example() def layout_columns( *, col_widths: BreakpointsUser[int] = None, @@ -393,6 +398,7 @@ def layout_columns( ) +@add_example() def card( *, full_screen: bool = False, @@ -452,6 +458,7 @@ def card( ) +@add_example() def card_header( *args: TagChild | TagAttrs, container: TagFunction = ui.tags.div, @@ -487,6 +494,7 @@ def card_header( ) +@add_example() def card_footer( *args: TagChild | TagAttrs, **kwargs: TagAttrValue, @@ -518,6 +526,7 @@ def card_footer( ) +@add_example() def accordion( *, id: Optional[str] = None, @@ -572,6 +581,7 @@ def accordion( ) +@add_example() def accordion_panel( title: TagChild, *, @@ -612,6 +622,7 @@ def accordion_panel( # ====================================================================================== +@no_example() def navset_tab( *, id: Optional[str] = None, @@ -648,6 +659,7 @@ def navset_tab( ) +@no_example() def navset_pill( *, id: Optional[str] = None, @@ -684,6 +696,7 @@ def navset_pill( ) +@no_example() def navset_underline( *, id: Optional[str] = None, @@ -721,6 +734,7 @@ def navset_underline( ) +@add_example() def navset_hidden( *, id: Optional[str] = None, @@ -757,6 +771,7 @@ def navset_hidden( ) +@no_example() def navset_card_tab( *, id: Optional[str] = None, @@ -799,6 +814,7 @@ def navset_card_tab( ) +@no_example() def navset_card_pill( *, id: Optional[str] = None, @@ -841,6 +857,7 @@ def navset_card_pill( ) +@no_example() def navset_card_underline( *, id: Optional[str] = None, @@ -887,6 +904,7 @@ def navset_card_underline( ) +@no_example() def navset_pill_list( *, id: Optional[str] = None, @@ -931,6 +949,7 @@ def navset_pill_list( ) +@no_example() def navset_bar( *, title: TagChild, @@ -1029,6 +1048,7 @@ def navset_bar( ) +@add_example() def nav_panel( title: TagChild, *, @@ -1063,6 +1083,7 @@ def nav_panel( ) +@no_example() def nav_control() -> RecallContextManager[NavPanel]: """ Context manager for a control in the navigation container. @@ -1072,6 +1093,7 @@ def nav_control() -> RecallContextManager[NavPanel]: return RecallContextManager(ui.nav_control) +@no_example() def nav_menu( title: TagChild, *, @@ -1114,6 +1136,7 @@ def nav_menu( # ====================================================================================== # Value boxes # ====================================================================================== +@add_example() def value_box( *, showcase: Optional[TagChild] = None, @@ -1199,6 +1222,7 @@ def value_box( # ====================================================================================== +@no_example() def panel_well(**kwargs: TagAttrValue) -> RecallContextManager[Tag]: """ Context manager for a well panel @@ -1216,6 +1240,7 @@ def panel_well(**kwargs: TagAttrValue) -> RecallContextManager[Tag]: ) +@add_example() def panel_conditional( condition: str, **kwargs: TagAttrValue, @@ -1259,6 +1284,7 @@ def panel_conditional( ) +@no_example() def panel_fixed( *, top: Optional[str] = None, @@ -1305,6 +1331,7 @@ def panel_fixed( ) +@add_example() def panel_absolute( *, top: Optional[str] = None, @@ -1399,6 +1426,7 @@ def panel_absolute( # ====================================================================================== +@add_example() def tooltip( *, id: Optional[str] = None, @@ -1437,6 +1465,7 @@ def tooltip( ) +@add_example() def popover( *, title: Optional[TagChild] = None, diff --git a/shiny/express/ui/_hold.py b/shiny/express/ui/_hold.py index 46bae144f..b6fe8d295 100644 --- a/shiny/express/ui/_hold.py +++ b/shiny/express/ui/_hold.py @@ -6,6 +6,7 @@ from htmltools import wrap_displayhook_handler +from ..._docstring import no_example from ..._typing_extensions import ParamSpec __all__ = ("hold",) @@ -15,6 +16,7 @@ CallableT = TypeVar("CallableT", bound=Callable[..., object]) +@no_example() def hold() -> HoldContextManager: """Prevent the display of UI elements in various ways. diff --git a/shiny/express/ui/_page.py b/shiny/express/ui/_page.py index 5ebe2f75c..d005540e9 100644 --- a/shiny/express/ui/_page.py +++ b/shiny/express/ui/_page.py @@ -5,6 +5,7 @@ from htmltools import Tag from ... import ui +from ..._docstring import no_example from ...types import MISSING, MISSING_TYPE from .._recall_context import RecallContextManager from .._run import get_top_level_recall_context_manager @@ -16,6 +17,7 @@ def page_auto_cm() -> RecallContextManager[Tag]: return RecallContextManager(ui.page_auto) +@no_example() def page_opts( *, title: str | MISSING_TYPE = MISSING, diff --git a/shiny/reactive/_core.py b/shiny/reactive/_core.py index 9dfa64156..a20c49968 100644 --- a/shiny/reactive/_core.py +++ b/shiny/reactive/_core.py @@ -200,7 +200,6 @@ def isolate(self) -> Generator[None, None, None]: @add_example() -@no_example("express") @contextlib.contextmanager def isolate() -> Generator[None, None, None]: """ diff --git a/shiny/reactive/_reactives.py b/shiny/reactive/_reactives.py index 60c003dfa..10061a2e1 100644 --- a/shiny/reactive/_reactives.py +++ b/shiny/reactive/_reactives.py @@ -30,7 +30,7 @@ ) from .. import _utils -from .._docstring import add_example, no_example +from .._docstring import add_example from .._utils import is_async_callable, run_coro_sync from .._validation import req from ..types import MISSING, MISSING_TYPE, ActionButtonValue, SilentException @@ -679,7 +679,6 @@ def effect( @add_example() -@no_example("express") def effect( fn: Optional[EffectFunction | EffectFunctionAsync] = None, *, diff --git a/shiny/types.py b/shiny/types.py index 09b722492..e116e8454 100644 --- a/shiny/types.py +++ b/shiny/types.py @@ -16,7 +16,7 @@ from htmltools import TagChild -from ._docstring import add_example, no_example +from ._docstring import add_example from ._typing_extensions import NotRequired, TypedDict if TYPE_CHECKING: @@ -56,7 +56,6 @@ class FileInfo(TypedDict): @add_example(ex_dir="./api-examples/output_image") -@no_example("express") class ImgData(TypedDict): """ Return type for :class:`~shiny.render.image`. @@ -81,7 +80,6 @@ class ImgData(TypedDict): @add_example() -@no_example("express") class SafeException(Exception): """ Throw a safe exception. @@ -96,7 +94,6 @@ class SafeException(Exception): @add_example() -@no_example("express") class SilentException(Exception): """ Throw a silent exception. @@ -120,7 +117,6 @@ class SilentException(Exception): @add_example() -@no_example("express") class SilentCancelOutputException(Exception): """ Throw a silent exception and don't clear output