diff --git a/_quarto.yml b/_quarto.yml index 5a84587e..086afaff 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -19,6 +19,8 @@ project: - api/core - api/testing - templates + - tutorials + - get-started resources: - /pypi/** - /robots.txt @@ -73,12 +75,14 @@ website: logo-alt: The logo for Shiny for Python search: true left: - - text: "Learn Shiny" - file: docs/overview.qmd + - text: "Get Started" + file: get-started/welcome.qmd + - text: "Concepts" + file: docs/ui-overview.qmd - text: "Components" menu: - text: "Components" - file: components/index.qmd + href: components/index.qmd icon: sliders - text: "Layouts" file: layouts/index.qmd @@ -86,15 +90,8 @@ website: - text: "Templates" file: templates/index.qmd icon: code-square - - text: "Deploy" - menu: - - text: "Overview" - href: docs/deploy.qmd - - docs/deploy-cloud.qmd - - docs/deploy-on-prem.qmd - - docs/shinylive.qmd - text: "Gallery" - file: gallery/index.qmd + href: gallery/index.qmd - text: "Playground" href: https://shinylive.io/py/examples/ target: _blank @@ -227,7 +224,28 @@ website: - text: "Controlling for Page Size" href: "/layouts/arrange/index.html#controlling-for-page-width-and-height" - - id: docs + - id: get-started + style: "floating" + collapse-level: 1 + align: left + contents: + - get-started/welcome.qmd + - get-started/install.qmd + - get-started/create-run.qmd + - get-started/debug.qmd + - section: "Deploy" + contents: + - text: "Overview" + href: get-started/deploy.qmd + - get-started/deploy-cloud.qmd + - get-started/deploy-on-prem.qmd + - get-started/shinylive.qmd + - section: "Tutorials" + contents: + - text: "Get Started with Shiny Express" + href: tutorials/intro-express/1-welcome.qmd + + - id: concepts style: "floating" collapse-level: 2 align: left @@ -236,10 +254,6 @@ website: contents: - docs/overview.qmd - docs/user-interfaces.qmd - - section: "💻 __Workflow__" - contents: - - docs/install-create-run.qmd - - docs/debug.qmd - section: "🎨 __User interfaces__" contents: - docs/ui-overview.qmd @@ -278,6 +292,23 @@ website: contents: - docs/nonblocking.qmd - docs/routing.qmd + + - id: tutorial-express-intro + style: "docked" + align: left + contents: + - section: "Learn Shiny Express" + contents: + - tutorials/intro-express/1-welcome.qmd + - tutorials/intro-express/2-ui.qmd + - tutorials/intro-express/3-inputs.qmd + - tutorials/intro-express/4-external.qmd + - tutorials/intro-express/5-outputs.qmd + - tutorials/intro-express/6-reactivity.qmd + - tutorials/intro-express/7-publish.qmd + - tutorials/intro-express/8-next.qmd + + # TODO: if the sidebar only has 1 entry, then it displays for the entire site... # added entry below to prevent this. - id: deploy diff --git a/docs/_metadata.yml b/docs/_metadata.yml index 8eae132d..16b1a700 100644 --- a/docs/_metadata.yml +++ b/docs/_metadata.yml @@ -1 +1 @@ -sidebar: get-started +sidebar: concepts diff --git a/docs/comp-streamlit.quarto_ipynb b/docs/comp-streamlit.quarto_ipynb new file mode 100644 index 00000000..42a70731 --- /dev/null +++ b/docs/comp-streamlit.quarto_ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: Streamlit\n", + "format:\n", + " html:\n", + " code-line-numbers: true\n", + " code-annotations: hover\n", + "include-in-header:\n", + " text: \"\"\n", + "---" + ], + "id": "eb526a4f" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| echo: false\n", + "from helpers import express_editor_tabs" + ], + "id": "68d8ddbe", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The idea of Streamlit is to simplify application development by rerunning the entire application script whenever any user input changes.\n", + "This strategy leads to a great initial user experience, but quickly becomes constricting as your application grows in scope.\n", + "\n", + "Shiny and Streamlit differ in a few key ways:\n", + "\n", + "1) Shiny's reactive execution means that elements are minimally re-rendered.\n", + "2) You can build large Shiny applications without manually managing application state or caching data.\n", + "3) Shiny allows you to easily customize the look and feel of your application.\n", + "\n", + "Shiny is designed to support your application's growth without extensive rewriting; the patterns you learn when developing a simple app are robust enough to handle a complicated one.\n", + "\n", + "## Streamlit example\n", + "\n", + "Consider this basic Streamlit application which filters a dataset and draws two plots.\n", + "The nice thing about this application is that it's very similar to a non-interactive script.\n", + "This makes getting started very easy because all you need to do to turn this script into an application is to add some Streamlit function calls to your variables and outputs.\n", + "At the beginning, Streamlit doesn't demand that you change your code to fit into a particular structure.\n", + "\n", + "The way Streamlit achieves this is by rerunning your script from start to finish every time the user takes an action.\n", + "While this works okay for small applications it is inefficient, and becomes intractable for larger more complicated ones.\n", + "In this case clicking the `Add Smoother` button will cause the entire app to reload, even though the button is only used by one plot.\n", + "\n", + ":::: {.grid .column-screen-inset}\n", + "::: {.g-col-12 .g-col-md-6 .col-code}\n", + "``` {.python}\n", + "import streamlit as st\n", + "import pandas as pd\n", + "from plotnine import ggplot, geom_density, aes, theme_light, geom_point, stat_smooth\n", + "from pathlib import Path\n", + "\n", + "infile = Path(__file__).parent / \"penguins.csv\"\n", + "df = pd.read_csv(infile)\n", + "\n", + "\n", + "def dist_plot(df):\n", + " plot = (\n", + " ggplot(df, aes(x=\"Body Mass (g)\", fill=\"Species\"))\n", + " + geom_density(alpha=0.2)\n", + " + theme_light()\n", + " )\n", + " return plot.draw()\n", + "\n", + "\n", + "def scatter_plot(df, smoother):\n", + " plot = (\n", + " ggplot(\n", + " df,\n", + " aes(\n", + " x=\"Bill Length (mm)\",\n", + " y=\"Bill Depth (mm)\",\n", + " color=\"Species\",\n", + " group=\"Species\",\n", + " ),\n", + " )\n", + " + geom_point()\n", + " + theme_light()\n", + " )\n", + "\n", + " if smoother:\n", + " plot = plot + stat_smooth()\n", + "\n", + " return plot.draw()\n", + "\n", + "\n", + "with st.sidebar:\n", + " mass = st.slider(\"Mass\", 2000, 8000, 6000)\n", + " smoother = st.checkbox(\"Add Smoother\")\n", + "\n", + "filt_df = df.loc[df[\"Body Mass (g)\"] < mass]\n", + "\n", + "st.pyplot(scatter_plot(filt_df, smoother))\n", + "st.pyplot(dist_plot(filt_df))\n", + "```\n", + ":::\n", + "::: {.g-col-12 .g-col-md-6}\n", + "![](assets/streamlit-penguins.mp4)\n", + ":::\n", + "::::\n", + "\n", + "## Shiny translation\n", + "\n", + "Shiny express apps look very similar to Streamlit apps, but run much more efficiently.\n", + "Unlike Streamlit, Shiny does not rerender the application every time an input is changed, but instead keeps track of the relationships between components and minimally rerenders the components which need to be updated.\n", + "The framework does this automatically when the application is run, and so you don't need to manually define the execution method for your app.\n", + "\n", + ":::{.column-screen-inset}" + ], + "id": "e1f9c519" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| output: asis\n", + "#| echo: false\n", + "\n", + "express_editor_tabs(\"apps/comp-streamlit/penguins\")" + ], + "id": "d6269081", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + ":::\n", + "\n", + "\n", + "The main difference between Streamlit and Shiny is code organization.\n", + "Since Streamlit runs everything from top to bottom it doesn't particularly matter how your code is organized.\n", + "In order to benefit from Shiny's execution model, you need to organize your code into decorated functions.\n", + "\n", + "For example, take this part of the application code:\n", + "\n", + "``` python\n", + "@reactive.calc\n", + "def filtered_data():\n", + " filt_df = df.copy()\n", + " filt_df = filt_df.loc[df[\"body_mass_g\"] < input.mass()]\n", + " return filt_df\n", + "\n", + "@render.plot\n", + "def mass_distribution():\n", + " return dist_plot(filtered_data())\n", + "\n", + "@render.plot\n", + "def scatter():\n", + " return scatter_plot(filtered_data(), input.smoother())\n", + "```\n", + "\n", + "These functions define the three main nodes of the application, as well as the relationships between them.\n", + "The `@render.plot` and `@reactive.calc` decorators identify the functions as reactive functions which need to re-execute in response to upstream changes, and the `filtered_data()` and `input.*` calls define the relationships between these components.\n", + "The decorators allow Shiny to construct a computation graph of the application as it runs, and only rerender an element when one of its upstream dependencies changes.\n", + "\n", + "\n", + "```{mermaid}\n", + "flowchart LR\n", + " S[input.mass] --> F[Filtered Data]\n", + " F --> H((Distribution))\n", + " F --> SC((Scatterplot))\n", + " C[input.smoother] --> SC\n", + "```\n", + "\n", + "\n", + "# Extending the application\n", + "\n", + "Organizing your app this way means that you can extend the application without rewriting it.\n", + "For example, let's add a button which resets the slider.\n", + "In Shiny you can do this by adding a `@reactive.effect` function which calls the `ui.update_slider()` function.\n", + "This adds a node to the computation graph and everything works as you'd expect it to.\n", + "Importantly, we can extend the application without changing how we think about the overall application.\n", + "\n", + "\n", + "```{mermaid}\n", + "flowchart LR\n", + " S[input.mass] --> F[Filtered Data]\n", + " F --> H((Distribution))\n", + " F --> SC((Scatterplot))\n", + " C[input.smoother] --> SC\n", + " R{Reset} -.-> S\n", + "```\n", + "\n", + "\n", + ":::{.column-screen-inset}" + ], + "id": "41d4ae47" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| echo: false\n", + "#| output: asis\n", + "\n", + "express_editor_tabs(\"apps/comp-streamlit/slider-update\")" + ], + "id": "87297724", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + ":::\n", + "\n", + "\n", + "### Streamlit requires rewriting\n", + "\n", + "Streamlit is optimized for very simple applications, but the cost of that is that Streamlit applications can be quite challenging to extend.\n", + "For example, to add a reset button to Streamlit you might expect that something like this would work.\n", + "After all, if your script runs from top-to-bottom whenever a button is pressed, shouldn't you be able to redefine a slider using an `if` statement?\n", + "\n", + "```{.python}\n", + "import streamlit as st\n", + "\n", + "x = st.slider(\"x\", 0, 10, 5)\n", + "btn = st.button(\"Reset\")\n", + "if btn:\n", + " x = st.slider(\"x\", 0, 10, 5)\n", + "```\n", + "\n", + "Unfortunately, this doesn't work because Streamlit maintains hidden application state, and resetting the slider value causes a name conflict.\n", + "In order to get this to work you need to first initialize a state variable `slider` which matches the `key` of the slider input widget, then you need to define a callback function and pass that as an argument to the button function.\n", + "Streamlit then uses the slider `key` to look for a variable with that same key session state.\n", + "This variable defines the value of the slider.\n", + "\n", + "The difficulty here is that in order to get the app to work you need to change your mental model of how the application runs.\n", + "Instead of thinking about your app as a simple Python script which reruns when anything changes, you need to start thinking about manually manipulating the state variables which persist across runs.\n", + "The limitations of the simple rerun-everything model will require you to add more and more workarounds like this as your application grows in complexity.\n", + "\n", + "```{.python}\n", + "import streamlit as st\n", + "import pandas as pd\n", + "from plotnine import ggplot, geom_density, aes, theme_light, geom_point, stat_smooth\n", + "from pathlib import Path\n", + "\n", + "infile = Path(__file__).parent / \"penguins.csv\"\n", + "df = pd.read_csv(infile)\n", + "\n", + "\n", + "def dist_plot(df):\n", + " plot = (\n", + " ggplot(df, aes(x=\"Body Mass (g)\", fill=\"Species\"))\n", + " + geom_density(alpha=0.2)\n", + " + theme_light()\n", + " )\n", + " return plot.draw()\n", + "\n", + "\n", + "def scatter_plot(df, smoother):\n", + " plot = (\n", + " ggplot(\n", + " df,\n", + " aes(\n", + " x=\"Bill Length (mm)\",\n", + " y=\"Bill Depth (mm)\",\n", + " color=\"Species\",\n", + " group=\"Species\",\n", + " ),\n", + " )\n", + " + geom_point()\n", + " + theme_light()\n", + " )\n", + "\n", + " if smoother:\n", + " plot = plot + stat_smooth()\n", + "\n", + " return plot.draw()\n", + "\n", + "# You need to check for the variable in session state to avoid an error\n", + "if \"slider\" not in st.session_state:\n", + " st.session_state[\"slider\"] = 6000\n", + "\n", + "def reset_value():\n", + " st.session_state[\"slider\"] = 6000\n", + "\n", + "\n", + "with st.sidebar:\n", + " mass = st.slider(\n", + " label=\"Mass\",\n", + " min_value=2000,\n", + " max_value=8000,\n", + " key=\"slider\", # The `key` imports the number which is stored in `session_state`\n", + " )\n", + " smoother = st.checkbox(\"Add Smoother\")\n", + " reset = st.button(\"Reset Slider\", on_click=reset_value)\n", + "\n", + "filt_df = df.loc[df[\"Body Mass (g)\"] < mass]\n", + "\n", + "st.pyplot(scatter_plot(filt_df, smoother))\n", + "st.pyplot(dist_plot(filt_df))\n", + "```\n", + "\n", + "\n", + "# Customizing UI\n", + "\n", + "Shiny embraces [UI as HTML](ui-html.qmd), and as a result it's relatively easy to implement bespoke [UI customizations](ui-customize.html).\n", + "For example, lets change the color of one button without changing the colors of any other buttons in our app.\n", + "Since Shiny allows you to add HTML attributes like `class`/`style`, and provides a CSS framework ([Bootstrap](https://getbootstrap.com/docs/5.3/getting-started/introduction/)), we can make primary button by just adding an appropriate `class` attribute.\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| components: [editor, viewer]\n", + "#| layout: vertical\n", + "#| viewerHeight: 100\n", + "from shiny.express import ui\n", + "\n", + "ui.input_action_button(\"default\", \"Default Button\")\n", + "ui.input_action_button(\"primary\", \"Primary Button\", class_=\"btn-outline-primary\")\n", + "```\n", + "\n", + "You might not need to customize the CSS of your app that often, but it's important to have the option if your application calls for it.\n", + "For example, suppose your company wants to publish your application publicly on their website, but in order to do that you need to make sure that it matches their style guide.\n", + "You can do that with Shiny because it supports the same styling patterns that your company is probably already using.\n", + "\n", + "### Streamlit\n", + "\n", + "This task is almost impossible in Streamlit, and requires a [JavaScript workaround](https://discuss.streamlit.io/t/issues-with-background-colour-for-buttons/38723/2).\n", + "\n", + "```{.python}\n", + "import streamlit as st\n", + "import streamlit.components.v1 as components\n", + "\n", + "st.button(\"red\", \"Red Button\")\n", + "st.button(\"white\", \"White Button\")\n", + "\n", + "\n", + "def ChangeButtonColour(widget_label, font_color, background_color=\"transparent\"):\n", + " htmlstr = f\"\"\"\n", + " \n", + " \"\"\"\n", + " components.html(f\"{htmlstr}\", height=0, width=0)\n", + "\n", + "\n", + "ChangeButtonColour(\"red\", \"white\", \"red\")\n", + "```\n", + "\n", + "Despite its complexity, this is the best way to change the style of an individual element in Streamlit.\n", + "How this pattern works is:\n", + "\n", + "- Return an empty html component with a script tag\n", + "- Use that script to break out of the iframe and access the parent document\n", + "- Search through the parent elements for those which matches a string\n", + "- Change the style of those elements\n", + "\n", + "This pattern is fairly tricky to understand, and can lead to some unexpected bugs.\n", + "For instance changes to page structure or button names can cause the styling to behave unpredictably.\n", + "\n", + "Streamlit was designed around simple applications which didn't require customized styling, so it's no surprise that this type of styling is difficult.\n", + "It is, however, an example of how the up-front simplicity of Streamlit has a significant cost when you go outside the boundaries of that simplicity.\n", + "The fact that this is a fairly common workaround is an indication that Streamlit users commonly exceed those boundaries.\n", + "\n", + "# Privacy and security\n", + "\n", + "Streamlit collects [user information](https://docs.streamlit.io/library/advanced-features/configuration#telemetry) on everyone who visits a running Streamlit app unless you opt-out.\n", + "The data is sent to a American server owned by Snowflake so that the company can analyze user behavior.\n", + "This can cause legal and security problems because your application may be subject to data governance policies which forbid this type of data collection.\n", + "For example, if your users do not explicitly provide consent to transfer data to a US company, sending data to Snowflake might be a [GDPR violation](https://github.com/streamlit/streamlit/issues/4747).\n", + "In order to prevent data collection you need to set `gatherUsageStats = false` in your Streamlit config file, which is an easy thing to forget to include in a given Streamlit deployment.\n", + "\n", + "Shiny does not collect or report user data of any kind, and it never will.\n", + "We do not believe that open-source tools should collect user data without explicit consent.\n", + "\n", + "\n", + "# Conclusion\n", + "\n", + "Shiny allows you to build much more performant and extensible applications than Streamlit.\n", + "The patterns that you use to build a simple Shiny application are the same ones that you use to build a complex one, and you never need to change your mental model of how the application works.\n", + "This design will let your application grow along with the scope of your problem, and you can have confidence that the framework has the tools that you need to handle almost any requirement." + ], + "id": "cd908e3c" + } + ], + "metadata": { + "kernelspec": { + "name": "pfe_book", + "language": "python", + "display_name": "pfe_book", + "path": "/Users/danielchen/Library/Jupyter/kernels/pfe_book" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/docs/install-create-run.qmd b/docs/install-create-run.qmd deleted file mode 100644 index a5471181..00000000 --- a/docs/install-create-run.qmd +++ /dev/null @@ -1,183 +0,0 @@ ---- -title: Install, create, & run -aliases: - - install.html ---- - -## Install - -`shiny` can be installed via `pip` or `conda`. - -::: {.panel-tabset .panel-pills} - -#### pip - -Before installing you may want to upgrade `pip` and install `wheel`: - -```bash -pip install --upgrade pip wheel -``` - -Next, install `shiny` from PyPI. - -```bash -pip install shiny -``` - -You may on occasion need to force installation of updated versions of our packages, since they are in development. This can be done with: - -```bash -pip install --upgrade shiny htmltools -``` - -::: {.callout-note collapse="true"} -##### Virtual environments - -For production apps, we recommend using a virtual environment to manage your dependencies. In this case, you should install `shiny` in your virtual environment scoped to your app, rather than globally. For example, if you are creating an app in a directory called `myapp`, you would create a virtual environment in that directory and install `shiny` there: - -```bash -mkdir myapp -cd myapp -# Create a virtual environment in the .venv subdirectory -python3 -m venv .venv -# Activate the virtual environment -source .venv/bin/activate -``` -::: - -::: {.callout-note collapse="true"} -##### Development versions - -If you want to install the development versions, you can do so with: - -```bash -pip install https://github.com/posit-dev/py-htmltools/tarball/main -pip install https://github.com/posit-dev/py-shiny/tarball/main -``` -::: - - -#### conda - -If you want to use a conda environment, feel free to create/activate one now: - -```bash -# Create a conda environment named 'myenv' -conda create --name myenv - -# Activate the virtual environment -conda activate myenv -``` - -Next, install `shiny` from conda-forge. - -```bash -conda install -c conda-forge shiny -``` - -You may on occasion need to force installation of updated versions of our packages, since they are in development. This can be done with: - -```bash -conda update -c conda-forge shiny -``` - -::: - -### VS Code - -We recommend installing the [Python][vscode-python] and [Shiny][vscode-shiny] extensions for [Visual Studio Code][vscode]. This provides, among other things, a play button in the top right corner of your editor that will run your Shiny app. - -If [type checking is important](https://john-tucker.medium.com/type-checking-python-306ad8339da1) to you, in addition to installing the [Python VSCode extension][vscode-python], you may want to do some additional configuration for a smooth experience with types in Shiny. See the tip below for more details. - -::: {.callout-tip collapse="true"} -##### Type checking - -We recommend the following settings in your project's `.vscode/settings.json` file: - -```default -{ - "python.analysis.typeCheckingMode": "basic", - "python.analysis.diagnosticSeverityOverrides": { - "reportUnusedFunction": "none" - } -} -``` - -or alternatively, if your project keeps these settings in `pyrightconfig.json`: - -```default -{ - "typeCheckingMode": "basic", - "reportUnusedFunction": "none", -} -``` - -The `basic` type checking mode will flag many potential problems in your code, but it does require an understanding of type hints in Python. This is the mode that is used by the [Shinylive](https://shinylive.io) examples editor. If you want to make even greater use of type checking, you can use `strict` mode: - -```default - "python.analysis.typeCheckingMode": "strict" -``` - -If you still find that too obtrusive and aren't used to working with type hints, you can remove that line entirely. - -In the above configuration, we also disable the `reportUnusedFunction` diagnostic, as it's idiomatic Shiny to create named functions that are never explicitly called by any code (i.e., `@reactive.effect`). - -You can also modify these settings on a per-file basis with comments at the top of the file. For example, you might have something like this at the top of your `app.py`: - -```default -# pyright: strict -# pyright: reportUnusedFunction=false -``` - -A full list of configuration settings for Pyright/Pylance is available [here](https://github.com/microsoft/pyright/blob/main/docs/configuration.md). - -[vscode]: https://code.visualstudio.com/ -[vscode-shiny]: https://marketplace.visualstudio.com/items?itemName=posit.shiny -[vscode-python]: https://marketplace.visualstudio.com/items?itemName=ms-python.python -::: - - -## Create - -The best way to create a new Shiny app is with the `shiny create` command line interface (CLI). This command asks you a series of questions about what kind of app you want to create, and then provides all the boilerplate code you need to get started with a working app. - -```bash -shiny create -``` - -![Running the shiny create command from a terminal](assets/shiny-create.mp4){class="img-shadow"} - -::: callout-tip -### Copy/paste examples - -If you find an example on this site that you want to run/edit locally, you can use `shiny create --template basic-app -m express` to get a basic app template, and then copy/paste the code from the example into the template. -::: - - -## Run - -Shiny apps can be launched from VSCode or the command line (via `shiny run`). - -### VS Code - -The best way to run (and develop) Shiny apps is in [Visual Studio Code][vscode] with the [Shiny extension][vscode-shiny]. When a Shiny `app.py` file is being edited, the default behavior of the Run button (circled in red in the screenshot below) becomes "Run Shiny App". - -![Visual Studio Code running with the Shiny extension](assets/vscode.png) - -This launches a Python process in a dedicated terminal instance, and a captive web browser. This lets you test your app without leaving your editor, and whenever you make changes to your app's source, the preview will update. To preview your app in a full browser, click the icon to the right of the URL bar to launch the app in an external browser. - -Next to the Run button is a dropdown menu that lets you "Debug Shiny App". This launches the app in debug mode, which lets you set breakpoints and step through your code. See the [debugging](debug.qmd) section for more information. - -### Command line - -To run a Shiny app from the command line, use the `shiny run` command. This command takes a single argument, the path to the app's entry point. For example, if your app's entry point is `app.py` in the directory `./app_dir`, you can run it like this: - -```bash -shiny run --reload --launch-browser app_dir/app.py -``` - -This should start your app and also automatically launch a web browser. - -The `--reload` flag means that file changes in the current directory tree will cause the Python process to restart and the browser to reload. Update and save changes to `app.py` and then wait a moment for the changes to appear in the browser. - -With these two `shiny` commands, you now have a project that you can run in your terminal. You can use any text editor or Python IDE to write Shiny apps, but we've taken special care to ensure a smooth workflow for [Visual Studio Code][vscode]. The next section will help you set up VS Code for Shiny for Python. diff --git a/docs/nonblocking.quarto_ipynb b/docs/nonblocking.quarto_ipynb new file mode 100644 index 00000000..714d769f --- /dev/null +++ b/docs/nonblocking.quarto_ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: Non-blocking operations\n", + "---\n", + "\n", + "\n", + "Sometimes in a Shiny app, you need to perform a long-running operation, like loading a large dataset or doing an expensive computation.\n", + "If you do this in a reactive context, it will block the rest of the application from running until the operation completes.\n", + "This can be frustrating for users, who may think that the app has crashed.\n", + "\n", + "Worse, if you have multiple users, then one user's long-running operation will block the other users' apps from running as well.\n", + "These other users will not even be aware that their slow performance is due to another user's actions.\n", + "\n", + "In this article, we'll learn how to make Shiny apps more responsive by using non-blocking operations.\n", + "We'll also go out of our way to explain why the usual Python async techniques don't work the same way in Shiny as in other web frameworks.\n", + "\n", + "## Using async/await in Shiny\n", + "\n", + "Asynchronous programming is a technique used in many programming languages to increase scalability and responsiveness, usually in programs that do a lot of networking like web servers and clients.\n", + "Python supports async programming at the language level, using the `async`/`await` keywords; in the standard library, with the [`asyncio`](https://docs.python.org/3/library/asyncio.html) module; and in third-party libraries, like [FastAPI](https://fastapi.tiangolo.com/) and [aiohttp](https://docs.aiohttp.org/).\n", + "\n", + "Shiny has async support as well, but it's a bit different from your typical async Python framework.\n", + "On the one hand, Shiny is built on top of [Starlette](https://www.starlette.io/), which is an async web framework, so it's possible to use async functions in many parts of your Shiny app.\n", + "On the other hand, Shiny is also designed around reactive programming concepts, and that creates constraints on how async functions can be used.\n", + "\n", + "### Reactive async/await\n", + "\n", + "Shiny supports the use of `async` and `await` in your reactive code.\n", + "You can use `async` functions in `@reactive.effect`, `@reactive.calc`, and render functions, and within those async functions you can use `await` to wait for the results of other async functions.\n", + "\n", + "However, you may be surprised to learn that this technique alone does not result in improved responsiveness in Shiny apps!\n", + "\n", + "In the app below, the first thing in the UI is a reactive output that displays the current time.\n", + "Click the button and notice that, during the five seconds that the app is waiting for the (artificially slow) sum calculation to complete, the time does not update.\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| components: [editor, viewer]\n", + "import asyncio\n", + "import datetime\n", + "from shiny import App, reactive, render, ui\n", + "\n", + "app_ui = ui.page_fluid(\n", + " ui.p(\"The time is \", ui.output_text(\"current_time\", inline=True)),\n", + " ui.hr(),\n", + " ui.input_numeric(\"x\", \"x\", value=1),\n", + " ui.input_numeric(\"y\", \"y\", value=2),\n", + " ui.input_task_button(\"btn\", \"Add numbers\"),\n", + " ui.output_text(\"sum\"),\n", + ")\n", + "\n", + "def server(input, output, session):\n", + " @render.text\n", + " def current_time():\n", + " reactive.invalidate_later(1)\n", + " return datetime.datetime.now().strftime(\"%H:%M:%S %p\")\n", + "\n", + " @reactive.calc\n", + " @reactive.event(input.btn)\n", + " async def sum_values():\n", + " await asyncio.sleep(5)\n", + " return input.x() + input.y()\n", + "\n", + " @render.text\n", + " async def sum():\n", + " return str(await sum_values())\n", + "\n", + "app = App(app_ui, server)\n", + "```\n", + "\n", + "Despite being defined as an asynchronous function, the sum logic is still blocking the time output.\n", + "You could replace `await asyncio.sleep(5)` with its synchronous equivalent, `time.sleep(5)`, and you'd get exactly the same result.\n", + "\n", + "### Why does async block?\n", + "\n", + "While surprising, this behavior is intentional.\n", + "Shiny goes out of its way to ensure that reactive functions are run in a serial fashion, never concurrently--even if they are asynchronous.\n", + "This means that if you have two (async) reactive effects that both call `await asyncio.sleep(1)`, the second one will not even begin to start executing until the first one has finished.\n", + "This is true even if the two reactive effects belong to two different Shiny sessions and one Python process is serving those sessions.\n", + "\n", + "This may seem like a strange decision on the part of Shiny: why support async at all if the code is not going to run concurrently?\n", + "\n", + "The reason for supporting async is simple: there are functions you may need to call from reactive contexts that are only available as async functions.\n", + "This includes some functions in Shiny itself that are used to communicate with the browser, like [`Session.send_custom_message`](api/Session.html#shiny.Session.send_custom_message).\n", + "\n", + "The reason for not allowing (reactive) async code to run concurrently is more nuanced.\n", + "The main reason is that it would be very difficult to ensure that the application behaves predictably if async code were allowed to run concurrently.\n", + "Concurrent code works best when tasks are largely independent of each other, and do not read or modify the same shared state.\n", + "But Shiny reactive code is all about shared state and interconnected tasks.\n", + "\n", + "So in summary, use async functions in your reactive code if you need to call async-only functions.\n", + "Don't expect your application to run faster, more responsively, or more efficiently.\n", + "\n", + "## True non-blocking behavior with `ExtendedTask`\n", + "\n", + "To achieve true non-blocking behavior in Shiny applications, and retain the ability to reason about how our apps will behave, we use the following strategy:\n", + "\n", + "1. Read whatever reactive inputs and calcs will be needed to perform the task.\n", + "2. Perform the task asynchronously and concurrently, outside of the reactive graph.\n", + "3. When the task completes, bring the resulting value (or error, if the operation failed) back into the reactive graph.\n", + "\n", + "We've created a high-level class called `ExtendedTask` to make all of this pretty easy.\n", + "To create an `ExtendedTask`, use the `@reactive.extended_task` decorator on an async function.\n", + "Let's adapt the example above to use an `ExtendedTask`:\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| components: [editor, viewer]\n", + "import asyncio\n", + "import datetime\n", + "from shiny import App, reactive, render, ui\n", + "\n", + "app_ui = ui.page_fluid(\n", + " ui.p(\"The time is \", ui.output_text(\"current_time\", inline=True)),\n", + " ui.hr(),\n", + " ui.input_numeric(\"x\", \"x\", value=1),\n", + " ui.input_numeric(\"y\", \"y\", value=2),\n", + " ui.input_task_button(\"btn\", \"Add numbers\"),\n", + " ui.output_text(\"sum\"),\n", + ")\n", + "\n", + "def server(input, output, session):\n", + " @render.text\n", + " def current_time():\n", + " reactive.invalidate_later(1)\n", + " return datetime.datetime.now().strftime(\"%H:%M:%S %p\")\n", + "\n", + " @ui.bind_task_button(button_id=\"btn\")\n", + " @reactive.extended_task\n", + " async def sum_values(x, y):\n", + " await asyncio.sleep(5)\n", + " return x + y\n", + "\n", + " @reactive.effect\n", + " @reactive.event(input.btn)\n", + " def btn_click():\n", + " sum_values(input.x(), input.y())\n", + "\n", + " @render.text\n", + " def sum():\n", + " return str(sum_values.result())\n", + "\n", + "app = App(app_ui, server)\n", + "```\n", + "\n", + "Note the `sum_values` function, which is where the actual (slow) work is done.\n", + "It is decorated with `@reactive.extended_task`, which means that it will be run asynchronously and concurrently with other tasks, and that its result will be available as `sum_values.result()`.\n", + "\n", + "We've also added a `@ui.bind_task_button` decorator on top of the `@reactive.extended_task` decorator.\n", + "\n", + "::: {.callout-note}\n", + "This synchronizes the `ExtendedTask` object with the `ui.input_task_button` in the UI, so that the button will be in its \"Processing...\" state while the task is running.\n", + "It _does not_ cause button clicks to invoke the task; we still need to do that manually, which we'll talk about next.\n", + "\n", + "If you use some other UI gesture or condition besides `ui.input_task_button` to invoke the task, don't include the `@ui.bind_task_button` decorator--it doesn't work with `ui.input_action_button`, for example.\n", + ":::\n", + "\n", + "### Invoking the task\n", + "\n", + "Unlike a reactive effect, simply creating an extended task does not cause it to run.\n", + "It needs to be invoked (called like a function).\n", + "\n", + "In this case, the `sum_values` extended task is called from the `btn_click` reactive effect (`@reactive.effect`), which runs whenever the button is clicked (`@reactive.event(input.btn)`).\n", + "\n", + "Notice also that the `sum_values` logic no longer reads `input.x()` and `input.y()` directly in the function body.\n", + "Because it is now an extended task, attempting to do so would result in an error.\n", + "Instead, it takes `x` and `y` as arguments, which are passed in by `btn_click` based on reactive inputs.\n", + "\n", + "### Retrieving results\n", + "\n", + "The `sum` output is now a regular reactive calc, which reads `sum_values.result()` to get the result of the extended task.\n", + "This output actually does not \"wait for\" the extended task to complete, exactly; instead, it will run multiple times, as the extended task goes through different states.\n", + "For each state, `sum_values.result()` will behave differently:\n", + "\n", + "* **Not yet invoked:** Raises a silent exception, which will cause the `sum` output to display nothing.\n", + "* **Running:** Raises a special type of exception that tells Shiny to keep the output in the \"in progress\" state.\n", + "* **Successfully completed:** Returns the return value of `sum_values`, in this case an integer.\n", + "* **Completed with an exception:** If `sum_values` raised an exception while processing, then re-raises that same exception, causing it to be displayed to the user in the `sum` output.\n", + "\n", + "It's not necessary to memorize these states.\n", + "Just remember that `sum_values.result()` is a reactive, synchronous method that knows how to do right thing based on the state of the extended task.\n", + "\n", + "### Other features of extended tasks\n", + "\n", + "#### Cancel a running task\n", + "\n", + "Although it's not shown in the example above, you can also cancel an extended task by calling the `cancel()` method (for example, `sum_values.cancel()`).\n", + "This will attempt to cancel the asyncio task that is running the extended task, and will cause `sum_values.result()` to raise a silent exception.\n", + "\n", + "Calling `sum_values.cancel()` on a task that isn't running will have no effect.\n", + "\n", + "#### Multiple invocations\n", + "\n", + "An extended task can run concurrently to reactive code and to other extended tasks--that's its whole purpose.\n", + "However, a single extended task object cannot run itself multiple times concurrently. If you call `sum_values()` while it is already running, it will enqueue the new invocation and run it after the first one completes.\n", + "\n", + "This is often not the behavior you want, especially if the task takes a long time to complete.\n", + "A user may accidentally click an action button twice, or they may click it again because they think the first click didn't work.\n", + "To prevent this, use `ui.input_task_button` instead of `ui.input_action_button` to invoke the task, since the former automatically prevents subsequent clicks until the task completes.\n", + "\n", + "### Executing on a different thread/process\n", + "\n", + "Extended task objects run their tasks using [`asyncio.create_task`](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task), which means that they will run on the same thread as the rest of the application.\n", + "This is fine for logic that is truly asynchronous, like making a network request via an asyncio-aware library like [aiohttp](https://docs.aiohttp.org/en/stable/), but it's not ideal for CPU-bound tasks or when performing I/O synchronously.\n", + "Because CPU-bound or synchronous tasks will block the main thread, we're back to where we started: the rest of the application cannot proceed until the task completes.\n", + "\n", + "Fortunately, we can rely on Python's built-in support for running asyncio tasks on different threads or processes.\n", + "\n", + "::: {.callout-note}\n", + "The examples below work well in Shiny Core, but a naive port to Shiny Express will not work as well.\n", + "The `ThreadPoolExecutor` and `ProcessPoolExecutor` objects need to be created as module-level variables, not as session-level variables, since we'd ideally like to pool resources across all sessions.\n", + ":::\n", + "\n", + "::: {.callout-note}\n", + "`ProcessPoolExecutor` is not available in Shinylive (e.g. WASM mode).\n", + "`ThreadPoolExecutor` is available in Shinylive and appears to work, but doesn't: it actually performs all of its work on the main thread.\n", + ":::\n", + "\n", + "The following example shows how to run a task on a different thread.\n", + "This is a good strategy for code that does synchronous I/O, like reading from disk, a database, or a remote API endpoint.\n", + "It's not as good of a strategy for CPU-bound code, because Python's [global interpreter lock](https://realpython.com/python-gil/) will prevent the task from running concurrently with other Python code.\n" + ], + "id": "c19ce53e" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| eval: false\n", + "import asyncio\n", + "import concurrent.futures\n", + "import time\n", + "\n", + "from shiny import App, reactive, render, ui\n", + "\n", + "app_ui = ui.page_fluid(\n", + " ui.input_numeric(\"x\", \"x\", value=1),\n", + " ui.input_numeric(\"y\", \"y\", value=2),\n", + " ui.input_task_button(\"btn\", \"Add numbers\"),\n", + " ui.output_text(\"sum\"),\n", + ")\n", + "\n", + "# Execute the extended task logic on a different thread. To use a different\n", + "# process instead, use concurrent.futures.ProcessPoolExecutor.\n", + "pool = concurrent.futures.ThreadPoolExecutor()\n", + "\n", + "def slow_sum(x, y):\n", + " time.sleep(5) # Simulate a slow synchronous task\n", + " return x + y\n", + "\n", + "def server(input, output, session):\n", + " @ui.bind_task_button(button_id=\"btn\")\n", + " @reactive.extended_task\n", + " async def sum_values(x, y):\n", + " loop = asyncio.get_event_loop()\n", + " return await loop.run_in_executor(pool, slow_sum, x, y)\n", + "\n", + " @reactive.effect\n", + " @reactive.event(input.btn)\n", + " def btn_click():\n", + " sum_values(input.x(), input.y())\n", + "\n", + " @render.text\n", + " def sum():\n", + " return str(sum_values.result())\n", + "\n", + "app = App(app_ui, server)\n", + "app.on_shutdown(pool.shutdown)" + ], + "id": "19537d3a", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With a small tweak, we can run the task on a different process instead of a different thread: just replace `concurrent.futures.ThreadPoolExecutor` with `concurrent.futures.ProcessPoolExecutor`.\n", + "This is a good strategy for CPU-bound code, because it allows the task to run concurrently with other Python code.\n", + "\n", + "In this example, the `slow_sum` function is defined at the module level, outside of the Shiny server function.\n", + "This is critically important for `ProcessPoolExecutor` to work correctly because of how Python's `multiprocessing` module works: only module-level functions can survive the trip to a worker Python subprocess.\n", + "(It's less critical for `ThreadPoolExecutor`, but still a good programming practice to define such logic at the module level when possible.)\n", + "\n", + "There's also the `app.on_shutdown(pool.shutdown)` line at the end of each example.\n", + "This is necessary to ensure that the pool is shut down when the app stops running.\n", + "In particular, if you're using `ProcessPoolExecutor` and neglect to shut down the pool, you can end up with orphaned Python processes hanging around.\n", + "\n", + "## Summary\n", + "\n", + "Only use async functions in your reactive code if you need to call async-only functions.\n", + "Don't expect your application to run faster, more responsively, or more efficiently.\n", + "\n", + "To achieve true non-blocking behavior in Shiny applications, factor your slow/async code into an `ExtendedTask` callable object, call it from a reactive effect, and read its `result()` from a render function or reactive calc." + ], + "id": "6ccf8b02" + } + ], + "metadata": { + "kernelspec": { + "name": "pfe_book", + "language": "python", + "display_name": "pfe_book", + "path": "/Users/danielchen/Library/Jupyter/kernels/pfe_book" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/docs/user-interfaces.qmd b/docs/user-interfaces.qmd index d1e4d524..57ea353f 100644 --- a/docs/user-interfaces.qmd +++ b/docs/user-interfaces.qmd @@ -1,5 +1,5 @@ --- -title: User interfaces +title: User Interfaces editor: markdown: wrap: sentence diff --git a/get-started/_metadata.yml b/get-started/_metadata.yml new file mode 100644 index 00000000..8eae132d --- /dev/null +++ b/get-started/_metadata.yml @@ -0,0 +1 @@ +sidebar: get-started diff --git a/docs/assets/dashboard-template.png b/get-started/assets/dashboard-template.png similarity index 100% rename from docs/assets/dashboard-template.png rename to get-started/assets/dashboard-template.png diff --git a/get-started/assets/positron-run.png b/get-started/assets/positron-run.png new file mode 100644 index 00000000..51adbdbc Binary files /dev/null and b/get-started/assets/positron-run.png differ diff --git a/docs/assets/tipping-dashboard.png b/get-started/assets/tipping-dashboard.png similarity index 100% rename from docs/assets/tipping-dashboard.png rename to get-started/assets/tipping-dashboard.png diff --git a/docs/assets/vscode.png b/get-started/assets/vscode.png similarity index 100% rename from docs/assets/vscode.png rename to get-started/assets/vscode.png diff --git a/get-started/create-run.qmd b/get-started/create-run.qmd new file mode 100644 index 00000000..ffe68b24 --- /dev/null +++ b/get-started/create-run.qmd @@ -0,0 +1,80 @@ +--- +title: "Create and Run" +--- + +When you [install shiny](get-started/install.qmd), you will have access to the `shiny` command line interface (CLI). +You can use this interface to help you create and run your Shiny applications. + +## Create a Shiny application + +The best way to create a new Shiny app is with the `shiny create` command line interface. +This command asks you a series of questions about what kind of app you want to create, +and then provides all the boilerplate code you need to get started with a working app. + +```bash +shiny create +``` + +![Running the shiny create command from a terminal](assets/shiny-create.mp4){class="img-shadow"} + +::: callout-tip +### Copy/paste examples + +If you find an example on this site that you want to run or edit locally, +you can use the following command to get a basic app template, +then copy and paste the code from the example into the template. + +```bash +shiny create --template basic-app -m express +``` +::: + + +## Run + +Shiny apps can be launched from Positron, VS Code, or the command line via `shiny run`. + +::: {.callout-tip} +#### Name your app `app.py` + +We recommend naming your shiny application `app.py`. +This is the default file that `shiny run` will look for, so you can run the application in the terminal without any additional parameters. + +If you need a more unique name, we recommend beginning the file with `app`, so the shiny extension can still run with a [play button click](install.qmd#positron). +::: + +### Positron and VS Code + +The best way to run (and develop) Shiny apps is in [Positron][positron] or [Visual Studio Code][vscode] with the [Shiny extension][vscode-shiny]. +When a Shiny `app.py` file is being edited, the default behavior of the Run button (circled in red in the screenshot below) becomes "Run Shiny App". + +![Visual Studio Code running with the Shiny extension](assets/positron-run.png) + +This launches a Python process in a dedicated terminal instance, and a captive web browser. +This lets you test your app without leaving your editor, and whenever you make changes to your app's source, the preview will update. To preview your app in a full browser, click the icon to the right of the URL bar to launch the app in an external browser. + +Next to the Run button is a dropdown menu that lets you "Debug Shiny App". This launches the app in debug mode, which lets you set breakpoints and step through your code. +See the [debugging](debug.qmd) section for more information. + +### Command line + +To run a Shiny app from the command line, use the `shiny run` command. +This command takes a single argument, the path to the app's entry point. +For example, if your app's entry point is `app.py` in the directory `./app_dir`, you can run it like this: + +```bash +shiny run --reload --launch-browser app_dir/app.py +``` + +This should start your app and also automatically launch a web browser. + +The `--reload` flag means that file changes in the current directory tree will cause the Python process to restart and the browser to reload. +Update and save changes to `app.py` and then wait a moment for the changes to appear in the browser. + +With these two `shiny` commands, you now have a project that you can run in your terminal. +You can use any text editor or Python IDE to write Shiny apps, but we've taken special care to ensure a smooth workflow for [Positron][positron] and [Visual Studio Code][vscode]. + +[positron]: https://positron.posit.co/ +[vscode]: https://code.visualstudio.com/ +[vscode-shiny]: https://marketplace.visualstudio.com/items?itemName=posit.shiny +[vscode-python]: https://marketplace.visualstudio.com/items?itemName=ms-python.python diff --git a/docs/debug.qmd b/get-started/debug.qmd similarity index 99% rename from docs/debug.qmd rename to get-started/debug.qmd index f78276ef..1e412638 100644 --- a/docs/debug.qmd +++ b/get-started/debug.qmd @@ -57,11 +57,9 @@ The error displayed in the app is only the final part of the stack trace, but th When Shiny apps are deployed, error messages are sanitized to the eliminate the possibility of leaking sensitive information. To unsanitize error messages, you'll need to set `sanitize_errors=True` in the `App` constructor (of a [Shiny core app](express-vs-core.qmd)). ::: - - ## Debugging -### VS Code debugger +### Positron and VS Code debugger The [VS Code debugger](https://code.visualstudio.com/docs/editor/debugging) is a powerful tool for debugging Python code. diff --git a/docs/deploy-cloud.qmd b/get-started/deploy-cloud.qmd similarity index 100% rename from docs/deploy-cloud.qmd rename to get-started/deploy-cloud.qmd diff --git a/docs/deploy-on-prem.qmd b/get-started/deploy-on-prem.qmd similarity index 100% rename from docs/deploy-on-prem.qmd rename to get-started/deploy-on-prem.qmd diff --git a/docs/deploy.qmd b/get-started/deploy.qmd similarity index 100% rename from docs/deploy.qmd rename to get-started/deploy.qmd diff --git a/get-started/install.qmd b/get-started/install.qmd new file mode 100644 index 00000000..5580a32f --- /dev/null +++ b/get-started/install.qmd @@ -0,0 +1,165 @@ +--- +title: Installation +--- + +Shiny for Python can be installed can be installed via `pip` or `conda`. + +::: {.panel-tabset .panel-pills} + +#### pip + +Before installing you may want to upgrade `pip` and install `wheel`: + +```bash +pip install --upgrade pip wheel +``` + + +Next, install `shiny` from PyPI. + +```bash +pip install shiny +``` + + +You may on occasion need to force installation of updated versions of our packages, since they are in development. +This can be done with: + +```bash +pip install --upgrade shiny htmltools +``` + +::: {.callout-note collapse="true"} +##### Virtual environments + +For production apps, we recommend using a virtual environment to manage your dependencies. +In this case, you should install `shiny` in your virtual environment scoped to your app, rather than globally. +For example, if you are creating an app in a directory called `myapp`, you would create a virtual environment in that directory and install `shiny` there: + +```bash +mkdir myapp +cd myapp + +# Create a virtual environment in the .venv subdirectory +python3 -m venv .venv + +# Activate the virtual environment +source .venv/bin/activate +``` +::: + +::: {.callout-note collapse="true"} +##### Development versions + +If you want to install the development versions, you can do so with: + + +```bash +pip install https://github.com/posit-dev/py-htmltools/tarball/main +pip install https://github.com/posit-dev/py-shiny/tarball/main +``` +::: + + +#### conda + +You can install `shiny` from conda-forge channel. + +```bash +conda install -c conda-forge shiny +``` + +You may on occasion need to force installation of updated versions of our packages, since they are in development. +This can be done with: + +```bash +conda update -c conda-forge shiny +``` + +::: {.callout-note collapse="true"} +##### Conda Virtual environments + +For production apps, we recommend using a virtual environment to manage your dependencies. +Create a conda environment with the code below before installing the `shiny` package. + +```bash +# Create a conda environment named 'shiny' +conda create --name shiny + +# Activate the virtual environment +conda activate shiny +``` +::: + +::: + +## Positron {#positron} + +We recommend using +[Positron](https://positron.posit.co/), +a next-generation data science focused fork of Visual Studio Code. + +If you already have VS Code installed, we still recommend giving Positron a try, +your extensions from VS Code will not conflict with your Positron extensions. +Positron already ships with many Python-focused VS Code extensions from Open VSX, +including Quarto, Jupyter Notebooks, and Pyright. + +To get started with Shiny for Python, you will need to install the +[Shiny Extension](https://open-vsx.org/extension/posit/shiny). +This provides, among other things, a play button in the top right corner of your editor that will run your Shiny app. + +![](assets/positron-run.png) + +## Visual Studio Code + +For for [Visual Studio Code][vscode], +you will need the [Python][vscode-python] and [Shiny][vscode-shiny] extensions. + +If [type checking is important](https://john-tucker.medium.com/type-checking-python-306ad8339da1) to you, in addition to installing the [Python VSCode extension][vscode-python], you may want to do some additional configuration for a smooth experience with types in Shiny. See the tip below for more details. + +::: {.callout-tip collapse="true"} +##### Type checking + +We recommend the following settings in your project's `.vscode/settings.json` file: + +```default +{ + "python.analysis.typeCheckingMode": "basic", + "python.analysis.diagnosticSeverityOverrides": { + "reportUnusedFunction": "none" + } +} +``` + +or alternatively, if your project keeps these settings in `pyrightconfig.json`: + +```json +{ + "typeCheckingMode": "basic", + "reportUnusedFunction": "none", +} +``` + +The `basic` type checking mode will flag many potential problems in your code, but it does require an understanding of type hints in Python. This is the mode that is used by the [Shinylive](https://shinylive.io) examples editor. If you want to make even greater use of type checking, you can use `strict` mode: + +```json + "python.analysis.typeCheckingMode": "strict" +``` + +If you still find that too obtrusive and aren't used to working with type hints, you can remove that line entirely. + +In the above configuration, we also disable the `reportUnusedFunction` diagnostic, as it's idiomatic Shiny to create named functions that are never explicitly called by any code (i.e., `@reactive.effect`). + +You can also modify these settings on a per-file basis with comments at the top of the file. For example, you might have something like this at the top of your `app.py`: + +```python +# pyright: strict +# pyright: reportUnusedFunction=false +``` + +A full list of configuration settings for Pyright/Pylance is available [here](https://github.com/microsoft/pyright/blob/main/docs/configuration.md). +::: + +[vscode]: https://code.visualstudio.com/ +[vscode-shiny]: https://marketplace.visualstudio.com/items?itemName=posit.shiny +[vscode-python]: https://marketplace.visualstudio.com/items?itemName=ms-python.python diff --git a/docs/shinylive-share-button.png b/get-started/shinylive-share-button.png similarity index 100% rename from docs/shinylive-share-button.png rename to get-started/shinylive-share-button.png diff --git a/docs/shinylive-share-modal.png b/get-started/shinylive-share-modal.png similarity index 100% rename from docs/shinylive-share-modal.png rename to get-started/shinylive-share-modal.png diff --git a/docs/shinylive-shiny-deployment-model.png b/get-started/shinylive-shiny-deployment-model.png similarity index 100% rename from docs/shinylive-shiny-deployment-model.png rename to get-started/shinylive-shiny-deployment-model.png diff --git a/docs/shinylive-shinylive-deployment-model.png b/get-started/shinylive-shinylive-deployment-model.png similarity index 100% rename from docs/shinylive-shinylive-deployment-model.png rename to get-started/shinylive-shinylive-deployment-model.png diff --git a/docs/shinylive.qmd b/get-started/shinylive.qmd similarity index 100% rename from docs/shinylive.qmd rename to get-started/shinylive.qmd diff --git a/get-started/shinylive.quarto_ipynb b/get-started/shinylive.quarto_ipynb new file mode 100644 index 00000000..8cbfec98 --- /dev/null +++ b/get-started/shinylive.quarto_ipynb @@ -0,0 +1,344 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: \"Shinylive: Shiny + WebAssembly\"\n", + "format: html\n", + "jupyter: python3\n", + "---\n", + "\n", + "\n", + "Shinylive allows you to run Shiny applications entirely in a web browser, without the need for a separate server running Python.\n", + "\n", + "The traditional way of deploying Shiny involves in a separate server and client: the server runs Python and Shiny, and clients connect via the web browser. Each client keeps an open websocket connection as long as they are using the application.\n", + "\n", + "![](shinylive-shiny-deployment-model.png){fig-alt=\"Traditional Shiny deployment\" width=500px}\n", + "\n", + "\n", + "When an application is deployed with Shinylive, Python and Shiny _run in the web browser_: the browser is effectively both the client and server for the application. There is a web server that serves files, but it does not run Python or Shiny---it can be a \"dumb\" static web server.\n", + "\n", + "![](shinylive-shinylive-deployment-model.png){fig-alt=\"Shinylive deployment\" width=500px}\n", + "\n", + "\n", + "If you've looked at any of the documentation on this web site, or have played with any of the [examples at shinylive.io](https://shinylive.io/py/examples/), you have already used Shinylive. The examples on this site (with a handful of exceptions) and the shinylive.io examples all run using Shinylive, meaning that they run in your web browser.\n", + "\n", + "This is all possible because of the magic of WebAssembly and Pyodide.\n", + "\n", + "* [WebAssembly (wasm)](https://webassembly.org/) is a binary format for compiled programs that can run in a web browser at near-native speeds.\n", + "* [Pyodide](https://pyodide.org/) is a port of Python and many packages, compiled to WebAssembly.\n", + "\n", + "\n", + "Applications deployed with Shinylive have some advantages and disadvantages compared to a traditional Shiny deployment. The advantages include:\n", + "\n", + "* No installation: No need to install Python or Shiny on a computer.\n", + "* Easy sharing: Share applications with just a URL.\n", + "* Easy deployment: Applications can be deployed to any static web hosting service.\n", + "* Easy scaling: Since applications can be served as static files on a \"dumb\" web server it is easy to scale to high traffic loads.\n", + "* Security: Because the code is not running on a server, it eliminates a class of potential security risks. The code runs in the client web browser's code sandbox, which a platform that has been battle tested over the years.\n", + "\n", + "Some of the disadvantages of using Shinylive deployments compared to traditional Shiny deployments:\n", + "\n", + "* Fewer packages: Not all Python packages are available in Pyodide.\n", + "* Large download size: The downloaded payload size may be significantly larger. Downloading Pyodide and Python packages to run a Shiny application requires about 13MB of data. Other packages can significantly increase the amount of data. For example, numpy is 7.5 MB, pandas is 13 MB, and matplotlib is 11.5 MB. However, all of this data will be cached by the browser so that it will load quickly in subsequent runs.\n", + "* No secrets: Code and data for the application must be sent to the browser, so it can't be kept secret from the user.\n", + "* Restricted network: For security reasons, the web browser itself imposes restrictions on network communication.\n", + "\n", + "For certain types of Shiny applications, some of the limitations can be worked around by pre-processing a data set and including it with the application.\n", + "\n", + "One important difference between traditional Shiny and Shinylive deployments is that compute power is shifted from the server to the client.\n", + "In many cases, the client browser will have more compute power than a server, especially since the compute power of the user's machine is not shared across multiple users.\n", + "However, in other cases, this can be a roadblock, such as when a powerful server is needed to perform very intensive computations or requires access to a private data store.\n", + "\n", + "\n", + "## Sharing and deploying Shinylive applications\n", + "\n", + "In this document, we'll use the terms _sharing_ and _deploying_ Shiny applications. When we talk about _sharing_, we're referring to a method of encoding the application in a URL so that others can run the application if they simply have the URL. Sharing an application via a URL does not require you to have a server---you can simply use the server at shinylive.io.\n", + "\n", + "When we talk about _deploying_ applications, we mean creating a set of files which are to be served up by a web server. This does require you to have a web server. For a traditional Shiny deployment, this means having a server that runs R or Python. For a Shinylive deployment, this only requires a server that can serve static files---it can be a \"dumb\" web server which does not run Python. For example you could deploy your application to [GitHub Pages](https://pages.github.com/) or [Netlify](https://www.netlify.com/).\n", + "\n", + "\n", + "### Sharing Shinylive applications\n", + "\n", + "The easiest way to share an application is to create it on the [Shinylive editor](https://shinylive.io/py/examples/), and then click on the \"Create share link\" button. This will encode the application in a URL, which you can then share with others.\n", + "\n", + "![](shinylive-share-button.png){fig-alt=\"Share button\" width=250px}\n", + "\n", + "The dialog box that appears will provide two links: one for the application in the Shinylive editor, and one with for the application running standalone.\n", + "\n", + "![](shinylive-share-modal.png){fig-alt=\"Share URLs\" width=700px}\n", + "\n", + "Here is an example of a Shiny application that is encoded in a share URL. This will lead to the application with an editor and Python console:\n", + "\n", + "[https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAa...](https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMAMwCdiYACAZwAsBLCbJjmVYnTJMAgujxMArhwA6EOWlQB9aUwC8UjligBzOEpoAbaQBMAFNIxsATGZlgAEhwlQIJpmTauA1iyY1BDzpsLh0mAGVObgBCewBKOLkFdHVRdDNFFWcmADlSOESIMABfAF0gA)\n", + "\n", + "If you want to share just the Shiny application, without the editor and console, use the other link, which contains `/app/` instead of `/editor/`:\n", + "\n", + "[https://shinylive.io/py/app/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAa...](https://shinylive.io/py/app/#code=NobwRAdghgtgpmAXGKAHVA6VBPMAaMAYwHsIAXOcpMAMwCdiYACAZwAsBLCbJjmVYnTJMAgujxMArhwA6EOWlQB9aUwC8UjligBzOEpoAbaQBMAFNIxsATGZlgAEhwlQIJpmTauA1iyY1BDzpsLh0mAGVObgBCewBKOLkFdHVRdDNFFWcmADlSOESIMABfAF0gA)\n", + "\n", + "\n", + "These URLs have a hash that includes `#code=...`. The code for the entire application is encoded in that hash. Notably, web browsers do _not_ send the hash to the web server, so the server actually never sees the content of the Shiny application.\n", + "\n", + "The sharing dialog shows how long the URL is, in bytes. If you want to share a link on Twitter, the maximum length of a URL is about 4000 bytes, and it will be shortened using their t.co service. If you use [bit.ly](https://bit.ly/), the maximum length is about 2000 bytes. These link shorteners redirect the user to the longer URL.\n", + "\n", + "#### Sharing with gists\n", + "\n", + "Another way of sharing Shinylive applications is by using a GitHub gist. For example, the gist here:\n", + "\n", + "[https://gist.github.com/wch/e62218aa28bf26e785fc6cb99efe8efe](https://gist.github.com/wch/e62218aa28bf26e785fc6cb99efe8efe)\n", + "\n", + "Can be run with Shinylive here:\n", + "\n", + "* Editor: [https://shinylive.io/py/editor/#gist=e62218aa28bf26e785fc6cb99efe8efe](https://shinylive.io/py/editor/#gist=e62218aa28bf26e785fc6cb99efe8efe)\n", + "* App: [https://shinylive.io/py/app/#gist=e62218aa28bf26e785fc6cb99efe8efe](https://shinylive.io/py/app/#gist=e62218aa28bf26e785fc6cb99efe8efe)\n", + "\n", + "Notice that the `#gist=...` part of the URL simply uses the ID of the gist.\n", + "\n", + "To create a gist, you can go to [gist.github.com/](https://gist.github.com/), or you can use GitHub's [`gh`](https://cli.github.com/) command-line tool to create a gist from files on disk. To do that, first install `gh`, then use [`gh gist create`](https://cli.github.com/manual/gh_gist_create):\n", + "\n", + "```bash\n", + "gh gist create --public app.py\n", + "```\n", + "\n", + "Sharing via gists has some important differences from sharing via encoded-app URL. If you use a gist, you can modify the gist, and the sharing URL will stay the same. If you are sharing an encoded-app URL, the URL itself contains the application code, so if you want modify the code, you will have to generate a new URL and share that.\n", + "\n", + "Sharing via GitHub gist may not be appropriate for all use cases, because the GitHub API has rate limits: for a given IP address, the GitHub API allows 60 requests per hour. So an end user would only be able to load Shinylive applications 60 times in an hour. And if there are many users behind a single IP address with network address translation, they collectively would have a limit of 60 requests per hour.\n", + "\n", + "If you are using GitHub gist for sharing, you can see your remaining requests at [https://api.github.com/rate_limit](https://api.github.com/rate_limit).\n", + "\n", + ":::{.callout-note}\n", + "The GitHub API has a much higher rate limit if the end user is authenticated, but Shinylive currently does not support authenticating with GitHub.\n", + ":::\n", + "\n", + "\n", + "### Deploying Shinylive applications\n", + "\n", + "#### With Quarto websites\n", + "\n", + "::: {.callout-note}\n", + "The section below describes how to embed Shinylive applications in a Quarto document -- they can be thought of as Shiny applets in that mode. As of November 2023, the pre-release version of Quarto can work in a different mode: it can generate dashboards where the entire page is a single Shiny application. See [this repository](https://github.com/wch/retirement-simulation-dashboard/) for an example and more information about how they work and how to deploy them. This page will be updated soon with more information about this mode.\n", + ":::\n", + "\n", + "The easiest way to deploy Shinylive applications is using the [quarto-shinylive](https://github.com/quarto-ext/shinylive) extension.\n", + "This extension allows you to embed Shiny apps into a quarto html document, and deploy those applications anywhere that can host quarto websites.\n", + "Once you have the extension installed, you can insert `shinylive-python` code blocks into the document.\n", + "\n", + "```{{shinylive-python}}\n", + "#| standalone: true\n", + "\n", + "from shiny import *\n", + "\n", + "app_ui = ui.page_fluid(\n", + " ui.input_slider(\"n\", \"N\", 0, 100, 40),\n", + " ui.output_text_verbatim(\"txt\"),\n", + ")\n", + "\n", + "def server(input, output, session):\n", + " @output\n", + " @render.text\n", + " def txt():\n", + " return f\"The value of n*2 is {input.n() * 2}\"\n", + "\n", + "app = App(app_ui, server)\n", + "```\n", + "\n", + "#### Without Quarto\n", + "\n", + "If you're not using Quarto, you'll need to export and deploy your application yourself.\n", + "This involves:\n", + "\n", + "* **Exporting** the application: Create a directory of files that includes the Shinylive distribution and the application code.\n", + "* **Deploying**: Upload that directory to a static web host.\n", + "\n", + "\n", + "There are many ways to deploy to a static web server. For example, you could deploy to Netlify or GitHub Pages, or use Posit Connect, as described later in this page.\n", + "\n", + "First, install the shinylive package:\n", + "\n", + "```bash\n", + "pip install shinylive\n", + "```\n", + "\n", + "Next, create a directory with a Shiny application. We'll use the `shiny create` command to create a basic application in a directory called `myapp/`.\n", + "\n", + "```bash\n", + "shiny create --dir myapp\n", + "```\n", + "\n", + "Pick a Shiny app template to create in the `myapp` directory.\n", + "Next, create the distribution with shinylive:\n", + "\n", + "```bash\n", + "shinylive export myapp site\n", + "```\n", + "\n", + "The resulting `site` directory will contain the following files (among others that are not shown for brevity):\n", + "\n", + "```default\n", + "site\n", + "├── app.json # The application's files serialized to JSON\n", + "├── index.html # A web page for the application\n", + "├── edit\n", + "│   └── index.html # A web page for an editor view of the application\n", + "├── shinylive-sw.js # Shinylive service worker\n", + "└── shinylive # Shinylive content\n", + "    └── pyodide # Pyodide files\n", + "```\n", + "\n", + "This directory can now be deployed to a static web hosting service.\n", + "\n", + "You can preview the application by serving the files in the `site` directory:\n", + "\n", + "```bash\n", + "python3 -m http.server --directory site 8008\n", + "```\n", + "\n", + "This will serve the files in the `site` directory on port 8008. Then point your browser at http://localhost:8008/. You can also see the application with an online editor by pointing your browser at http://localhost:8008/edit/. (Note that any changes to the files there are ephemeral---they won't be saved to disk.)\n", + "\n", + ":::{.callout-note}\n", + "To run a Shinylive application, the files must be served with a web server; simply pointing your browser to the files on disk will not work. This is because security restrictions in web browsers require some assets to be retrieved from a web server instead of from disk.\n", + ":::\n", + "\n", + "If you have multiple applications, you may want to export them in subdirectories of the site, so that they can all share the same Shinylive assets. You can do this with the `--subdir` option:\n", + "\n", + "```bash\n", + "shinylive export myapp1 site --subdir app1\n", + "shinylive export myapp2 site --subdir app2\n", + "```\n", + "\n", + "The `site/shinylive/pyodide/` directory will contain a Pyodide distribution containing just the Python packages needed to run the exported application(s). There are some cases where you may want to include other packages. For example, if you want users who visit the `edit/` URL to be able to load more packages. In order to include extra packages, you have two options:\n", + "\n", + "* Add a `requirements.txt` file to an application which lists the extra packages.\n", + "* Run `shinylive export myapp site --full-shinylive`. This will cause it to include all of the Python packages from the Shinylive distribution.\n", + "\n", + "\n", + ":::{.callout-note}\n", + "The Shinylive distribution is under rapid development, and the files in the distribution will change. The `shinylive export` command automatically downloads and caches a a copy of the Shinylive distribution on your computer. To make sure you are up to date, run:\n", + "\n", + "```bash\n", + "pip install shinylive --upgrade\n", + "shinylive assets remove # Remove old cached shinylive files\n", + "```\n", + "Then the next time you run `shinylive export`, it will download the latest version.\n", + ":::\n", + "\n", + "\n", + "#### Deploying to Posit Connect\n", + "\n", + "After creating the directory with the application and Shinylive bundle, you can deploy it to many different of static web hosting services.\n", + "Posit Connect is one of those options, and allows you to control over who can access the application.\n", + "\n", + "If you would like to deploy to a Posit Connect server, install and configure the `rsconnect-python` package as described in the [Deploy](deploy-on-prem.qmd) page.\n", + "Then you can deploy the application as a static website:\n", + "\n", + "```bash\n", + "rsconnect deploy html site\n", + "```\n", + "\n", + "\n", + "## Python packages\n", + "\n", + "The Shinylive distribution is built on Pyodide, and contains a number of additional packages on top of the standard Pyodide distribution.\n", + "\n", + "It is also possible to use other Python packages, provided that they are packaged as wheels, and contain no compiled code. Additionally, they must not use features that aren't available in Pyodide. For example, if a package has code that uses `urllib.request`, it won't work in Pyodide.\n", + "\n", + "### Installed packages\n" + ], + "id": "9746e076" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| echo: false\n", + "\n", + "import shinylive._deps\n", + "from IPython.display import Markdown\n", + "from tabulate import tabulate\n", + "\n", + "repodata = shinylive._deps._pyodide_lock_data()\n", + "info = repodata[\"info\"]\n", + "packages = repodata[\"packages\"]" + ], + "id": "35831a44", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The Shinylive distribution includes packages from Pyodide `{python} info[\"version\"]`, as well as some additional Shiny-related packages. See [this page](https://pyodide.org/en/`{python} info[\"version\"]`/usage/packages-in-pyodide.html) for a list of packages included in Pyodide.\n", + "\n", + "Shinylive includes the following packages. Most are part of the Pyodide distribution, and a few of them are added by Shinylive.\n" + ], + "id": "eb836daf" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| echo: false\n", + "\n", + "packages = dict(sorted(packages.items()))\n", + "packages = {name: (info[\"name\"], info[\"version\"]) for name, info in packages.items()}\n", + "package_table = [info for name, info in packages.items()]\n", + "\n", + "# Todo: add table-sm and table-striped classes\n", + "Markdown(tabulate(\n", + " package_table,\n", + " headers=[\"Package\", \"Version\"],\n", + "))" + ], + "id": "bf3431ca", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Testing whether a package is available\n", + "\n", + "The Shinylive distribution includes many packages, but you may want to use one that is not included.\n", + "\n", + "It is possible to install packages using Pyodide's `micropip` package. To do that, simply visit the [Shinylive examples page](https://shinylive.io/py/examples/) and run the following in the Python console:\n", + "\n", + "```default\n", + "import micropip\n", + "await micropip.install(\"mypackage\")\n", + "import mypackage\n", + "```\n", + "\n", + "If that works without errors, then your package is usable in a Shinylive application. (There are some exceptions, where a package will load but not be fully usable in Pyodide.)\n", + "\n", + "The `micropip.install` command will install the package from PyPI by default. However, you can provide a URL that points directly to your package, like `https://example.com/mypackage-1.0-py3-none-any.whl`.\n", + "\n", + "\n", + "### Requiring extra packages with `requirements.txt`\n", + "\n", + "To use extra packages as part of your application, you can add a `requirements.txt` file to your application, as demonstrated in the [extra packages example](https://shinylive.io/py/examples/#extra-packages). The format of the requirements.txt file is similar to a \"normal\" requirements.txt file. For example, it could look like this:\n", + "\n", + "```default\n", + "isodate\n", + "attrs==21.4.0\n", + "```\n", + "\n", + "Each time someone runs your Shiny application, their web browser will fetch those packages from PyPI. It will then install the packages to a virtual file system (VFS); when the user closes the page or navigates away from it, the VFS is discarded. If the user goes back and runs the application again, those files can be fetched from the browser cache instead of from PyPI." + ], + "id": "2c998b0a" + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)", + "path": "/Users/danielchen/.pyenv/versions/3.12.6/share/jupyter/kernels/python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/get-started/welcome.qmd b/get-started/welcome.qmd new file mode 100644 index 00000000..d47a0509 --- /dev/null +++ b/get-started/welcome.qmd @@ -0,0 +1,65 @@ +--- +title: "Welcome to Shiny" +editor: + markdown: + wrap: sentence +--- + +Shiny for Python is a powerful and beginner-friendly framework for building interactive web applications and dashboards, all in pure Python. +Whether you’re a data scientist, analyst, or developer, Shiny makes it easy to create rich, interactive experiences without needing to learn JavaScript or front-end frameworks. + +## Batteries included: everything you need to build a dashboard {#batteries-included} + +Shiny for Python comes fully equipped with everything you need to build a dashboard right out of the box. +It includes a rich set of [input and output components](/components) so you can quickly build interactive applications without worrying about external dependencies. +Layout options let you organize your UI efficiently, while built-in theming (including dark mode) ensures your app looks great with minimal effort. +You can even use a [brand.yml](https://posit-dev.github.io/brand-yml/) file to apply consistent branding, colors, and logos across your application. + +## Reactivity: the secret to seamless interactivity {#reactivity} + +At the heart of Shiny is [reactivity](../docs/reactive-foundations.qmd), a system that automatically updates outputs when inputs change. +Unlike traditional web apps, where you need to manually handle state and data updates with callbacks, Shiny’s reactive engine keeps everything in sync effortlessly. +For example, when a user interacts with a slider or selects a filter, reactivity figures out the **minimum** amount of calculations to update the outputs without requiring you to write complex event-handling code. +This makes Shiny ideal for fast data-driven applications, enabling live updates for charts, tables, and reports with minimal effort, +and gives you the confidence that results are accurately rendered. + +## Templates: Get started quickly {#templates} + +To help you hit the ground running, Shiny provides starter [templates](/templates/) for common use cases, such as data dashboards, applications, streaming updates, data entry. + +The `shiny create` command walks you through a series of prompts to help you get started quickly with a helpful example. +You can use this command to load up a basic dashboard: + +```bash +shiny create --template dashboard +``` + +![The resulting dashboard generated by the dashboard template](assets/dashboard-template.png){class="img-shadow"} + +## Extensibility: Customize and Expand as Needed {#extensible} + +While Shiny includes everything you need to build an app, is built on a foundation of web standards, making it highly extensible. +If you need a custom component or user interface, you can incrementally integrate JavaScript, HTML, or even WebAssembly without having to learn complicated build tooling and frameworks. + +The UI components themselves are built on a Python representation of HTML/CSS/JavaScript, which you can see by printing them in a Python REPL: + +```python +>>> from shiny import ui +>>> ui.input_action_button("btn", "Button") + +``` + +If you're versed in web programming, you can also use Shiny to create [custom components](../docs/custom-component-one-off.qmd) that leverage your favorite JavaScript framework from Python. + +## Why choose Shiny for Python? + +Why Choose Shiny for Python? + +- 🐍 No JavaScript required – Build full-featured interactive apps in pure Python. +- ⚡ Fast iteration – The reactive model allows quick and dynamic updates. +- 🎨 Built-in theming and layout – Make beautiful dashboards effortlessly. +- 🚀 Scalability and extensibility – Start simple, grow as needed. + +Shiny for Python empowers you to bring your data to life with interactive applications that are easy to build, customize, and share. + +Ready to give it a try? diff --git a/tutorials/index.qmd b/tutorials/index.qmd new file mode 100644 index 00000000..b2fa1e9c --- /dev/null +++ b/tutorials/index.qmd @@ -0,0 +1,124 @@ +--- +title: "Tutorials" +--- + +```{=html} + +``` + +## Express-Only Tutorials + +Shiny Express is the easiest and quickest way to start with Shiny. +Create applications at the speed of thought. + +```{=html} +
+ +

Getting Started with Shiny

+

Install and build your first Shiny Application.

+

Updated: Feb 14, 2024

+ Start Tutorial +
+
+``` + +## Express and Core Tutorials + +Here are the more general tutorials to get you started with all things Shiny. + +```{=html} +
+ +

Shiny Express to Shiny Core

+

Learn how to transition from Shiny Express to Shiny Core.

+

Updated: Feb 5, 2024

+ Start Tutorial +
+ +

Shiny Modules

+

Create Shiny Modules to make more maintanable applications.

+

Updated: Mar 15, 2024

+ Start Tutorial +
+ +

Make your own component

+

Create your own custom javasscript components.

+

Updated: Apr 20, 2024

+ Start Tutorial +
+ +

Testing your application

+

Write unit tests and end-to-end tests with pytest and playwright.

+

Updated: Apr 20, 2024

+ Start Tutorial +
+
+``` + +## Core-Only Tutorials + +Shiny Core provides you all the flexibility and complexity you need. +The syntax is a bit more verbose, +but you can create more complex applications using the core syntax. + +```{=html} +
+ +

Getting Started with Shiny

+

Install and build your first Shiny Application with the Core syntax.

+

Updated: Feb 14, 2024

+ Start Tutorial +
+
+``` diff --git a/tutorials/intro-express/1-welcome.qmd b/tutorials/intro-express/1-welcome.qmd new file mode 100644 index 00000000..1e8f9fa5 --- /dev/null +++ b/tutorials/intro-express/1-welcome.qmd @@ -0,0 +1,152 @@ +--- +title: Getting Started +--- + +Shiny for Python is a web application framework that helps tell your data story. +If you've landed on this page, you probably have a bit of Python experience, worked with data, and now need a way to publish an interactive web application to help tell your data story. + +## Installation and Setup + +This is a 1 to 2 Hour tutorial to get you started and familiar with all the basic parts of creating and deploying a Shiny for Python application. +Before starting this tutorial, check to make sure you have your packages and environment setup. +You can see the [Installation section of the Get Started Guides](/get-started/install-create-run.qmd). + +## Parts of a Shiny Application + +Shiny express allows us to write shiny apps with a minimal amount of code. +This lets us rapidly link interactive components with our data in our web application. + +There are 3 main parts of a shiny express application + +1. [input components](/components/#inputs): provide user interactions that can be used as inputs in other parts of the web application. +2. [output components](/components/#outputs): results that are displayed on the web application. +3. [layout and ui components](/layouts): how and where the inputs and output of the web application are displayed. + +The example below demonstrates the basic mechanics behind Shiny apps. +As you move the slider (an input component), the text (output component) will react and update to the corresponding input value. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 150 +from shiny.express import input, render, ui + +ui.input_slider(id="val", label="Slider label", min=0, max=100, value=50) + +@render.text +def slider_val(): + return f"Slider value: {input.val()}" +``` + +Let's briefly break down the components of the application above: + +- Inputs + - Create a slider with a `ui.input_*` function. + - Here we are using the `ui.input_slider()` function to create the slider. + - This slider has an id of `"val"` which is used to get the slider value later +- Outputs + - Created by decorating a function with the corresponding `@render.*` decorator. + - Here we are displaying text, so we are using the `@render.text` decorator. + - Inside a `render` function, `input` values can be read [reactively](#reactivity). + - We read the value from the slider by calling `input.val()`. + - When those `input` values change, Shiny knows how to minimally re-render output. +- Layouts + - Inferred automatically based on what items you place in your application. + - We will learn more about layouts and user interfaces in the next lesson of this tutorial. + +:::: callout-note +## Exercise + +Let's make and run our first shiny for python application. + +1. Take the above code and save it to a file. Here we named it `app.py` +2. Click on the play button (red circle in the image below)j + +You will see the terminal run the `shiny run` command for you automatically. +The output will look something like this + +``` bash +$ python -m shiny run --port 55901 --reload --autoreload-port 55902 app-010-simple.py +INFO: Will watch for changes in these directories: ['~/Desktop/py-shiny-example'] +INFO: Uvicorn running on http://127.0.0.1:55901 (Press CTRL+C to quit) +INFO: Started reloader process [24969] using WatchFiles +INFO: Started server process [24986] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: 127.0.0.1:56426 - "GET /?vscodeBrowserReqId=1737097751843 HTTP/1.1" 200 OK +``` + +This will run the application on port `55901` and automatically reload and update as you make changes to the `app.py` file. + +1. You will see the app build in the bottom terminal and open in the viewer on the side +2. Move the slider and see how the output reacts +3. Congratulations, you made your first shiny for python application! + +::: column-margin +![](img/010-run_app-cropped_editor.png.png) +::: +:::: + +::: callout-tip +## Naming your files + +If you start your file with the word `app`, the shiny for python extension will recognize it as an application and you will be able to see the "play" button to run your application. +You can also name your file `app-get_started.py` and you will still get the shiny extension play button. + +To have Shiny for Python work well with the VS Code extensions and for you to go through the next series of lessons. +We recommend either one of the following file naming conventions: + +1. Create separate folders for each app example you will create and save separate `app.py` files in each folder +2. Create separate `app*.py` files in the same directory (e.g., `app-01.py`, `app-02.py`) + +If you named your application `app.py` you can omit it form the command and only use `shiny run --reload`. +The `app.py` is the default file Shiny looks for to run in the current directory. +Otherwise, you can pass in the name of the file that you wish to run. +The `app` prefix used in the example above is used to signal to the Shiny VS Code extension to display the run app button. +::: + +## Run your shiny application + +In addition to the play button in Positron, you can manually run your application from the command line. +This is useful if you wish to specify your own port or want to rename your application without the `app` prefix. + +``` bash +shiny run my_app.py --reload +``` + +::: callout-tip +## Helpful run options + +Some useful options you can pass the `shiny run` command are: + +- `--reload`: Enables auto-reload, the application will reload to reflect your changes as you save your work. +- `--port`: pass in a custom port, e.g., `--port 8000`. This will run the app on the specified port, instead of a random port. This makes it easier to have the same browser window open as you stop and start your application. + +You can learn more about these run options on the [`run_app` documentation page](https://shiny.posit.co/py/api/core/run_app.html). +::: + +## Shiny Express: Your first application + +The rest of this tutorial will work on creating this [Restaurant Tipping Dashboard](https://gallery.shinyapps.io/template-dashboard-tips1/). + +:::: {.column-screen .hero-image .pt-4 .pb-5 style="margin-top:0px;max-width:1600px;"} +::: {.hello-output .g-col-12 .g-col-xl-12} + + +```{=html} + +``` +::: +:::: diff --git a/tutorials/intro-express/2-ui.qmd b/tutorials/intro-express/2-ui.qmd new file mode 100644 index 00000000..e0230819 --- /dev/null +++ b/tutorials/intro-express/2-ui.qmd @@ -0,0 +1,232 @@ +--- +title: User Interfaces and Layouts +--- + + + +In the previous lesson, +we saw how to create and run a basic shiny for python application. + +Now let's see how we can layout different user interfaces. + +If you are trying to whip up a quick application that needs input controls +and reactive outputs, shiny express tries to make this as simple as possible +by inferring the user interface and page layout for you. +However, +you have the option (and ability) to override these default layouts. + +We will assume that you have the pre-existing knowledge on how to +load a dataframe from a csv file, filter the data, calculate summaries from the data, +and plot results. +Here we will set the foundation on how to set up the UI that our code will +insert into to be displayed in a web application or dashboard. + +:::{.callout-tip} +When building your Shiny applications, +a general good practice is break up the application into two (2) separate steps + +1. Write the code for whatever interactive components you want + and use variables as place holders for the code. + It isn't interactive without code modifications yet, + but it'll help to make sure you have all the code working as you add interactive + components to it. +2. Outline the general user interface either literally on paper and/or put in + placeholder UI elements to get a sense of the look and feel of your application. + +You can do these steps in whatever asynchronously and in whatever order you'd like. +By keeping these steps separate, +especially as you are learning the framework, +you'll reduce the risks of creating errors and bugs that may be hard to point down. +::: + +## Layout User Interfaces + +We can lay out the input and output components on our web application using different +[shiny layouts](/layouts/). +We can have different navigation bars, sidebars, tabs, panels, and cards to control where each +component is displayed on the page. + +:::{.callout-note} +We will talk about the different input and output components separately in +later lessons of this tutorial. + +All the individual input components begin with a `input_*()` function. +You can find a list of all the inputs in the +[input components gallery](/components/#inputs). + +Outputs are created by decorating a function with the `@render.*()` decorator. +You can find a list of all the outputs in the +[outputs components gallery](/components/#outputs). +::: + + +Layouts in Shiny Express begin with the `with ui.*():` python context manager. +Here is an example of an Shiny Express application with a sidebar on the left. +One use case for this kind of layout is to provide the user the ability to interact +with components on the page +but also hide away the components to declutter +the application when they are not needed. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 150 +from shiny.express import ui + +with ui.sidebar(bg="#f8f8f8"): + "Sidebar" + +"Main content" +``` + +You can use navigation bars (navbars) to add different pages to your application. +Let's build on our current sidebar layout, +and add a navigation bar to the top of the application. +We can nest layouts by nesting the context managers. + +:::{.callout-note} +Context managers are not specific to Shiny for Python. +They are features and tools used thought the Python ecosystem. +Typically you will not have to write your own context manager in Python +and use the `with` statement for an existing context manager. + +For Shiny, all you need to remember that Shiny Express uses context managers +to layout each part of the user interface. +You can also nest shiny layout context managers, +but be mindful where the `with` statement is and where the indentations are. +::: + +## Page Layouts + +If you need to embed different page layouts, you will need to look for the `ui.layout_*()` functions. + +:::{.column-body-outset-right} +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 200 +from shiny.express import ui + +with ui.nav_panel("A"): + with ui.layout_sidebar(): + with ui.sidebar(id="sidebar_left", open="desktop"): + "Left sidebar content" + "Main content" + +with ui.nav_panel("B"): + "Page 2 content" + +with ui.nav_panel("C"): + "Page C content" +``` +::: + +## Restaurant Tips UI + +For a given page, there are a few ways you can +[layout specific elements](layouts/panels-cards/#content-divided-by-cards). + +Let's sketch out the basic outline of the shiny application. + +![](../../docs/assets/tipping-dashboard.png) + +:::{.callout-note} +## Exercise + +Let's replicate the restaurant tipping dashboard, +but only put in the UI elements. + +The Restaurant Tipping dashboard has the following parts: + +1. Title: "Restaurant tipping" + - You can use the [`ui.page_opts()`](https://shiny.posit.co/py/api/express/express.ui.page_opts.html) + and pass in a `title=''` parameter to add an application title. +1. Sidebar for a few input components (we'll add those later) + - You can put some text here as a place holder, e.g., `"sidebar inputs"` +2. A full width column with 3 value boxes + - Each value box will take up the same width of space + - The value boxes will have labels for "Total tippers", "Average tip", and "Average bill" + - The value boxes will need a placeholder value (we will populate them with a reactive value later) +3. A full width column with 2 cards, one for a dataframe and another for a scatter plot + - Each card will share the same width of space + - The care headers will have values of "Tips data" and "Total bill vs tip" +4. a full width column with 1 card + - The card has a header of "Tip percentages" + + +::::::{.callout-tip} +Here are the documentation pages for functions that may be useful for this exercise: + +- `ui.page_opts()`: +- `ui.sidebar()`: +- `ui.layout_columns()`: +- `ui.card()`: +- `ui.card_header()`: +:::::: +::: + +::: {.callout-caution collapse="true"} +## Solution + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 150 +from shiny.express import input, ui + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar (empty for now) +with ui.sidebar(open="desktop"): + "sidebar inputs" + +# body of application + +# first row of value boxes +with ui.layout_columns(fill=False): + with ui.value_box(): + "Total tippers" + 42 + + with ui.value_box(): + "Average tip" + 42 + + with ui.value_box(): + "Average bill" + 42 + +# second row of cards +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Tips data") + with ui.card(full_screen=True): + ui.card_header("Total bill vs tip") + +with ui.layout_columns(): + with ui.card(full_screen=True): + ui.card_header("Tip percentages") + +``` +::: + + +## Context managers + +You will typically not need to write your own context managers using the Python `with` statement. +If you would like to learn more about what context managers are, +and how to potentially write your own, you can check out +this +[context managers tutorial from Real Python](https://realpython.com/python-with-statement/) + +## Summary + +We have now created a skeleton for our dashboard by laying out the main UI components. +We will now be able to add input components. diff --git a/tutorials/intro-express/3-inputs.qmd b/tutorials/intro-express/3-inputs.qmd new file mode 100644 index 00000000..0b422a19 --- /dev/null +++ b/tutorials/intro-express/3-inputs.qmd @@ -0,0 +1,209 @@ +--- +title: Input Components +--- + + + +So far we've seen how to customize our user interface, +and saw how we can use layouts, cards, and the 12-Grid CSS Bootstrap layout +to help place different elements on our web application. +Now let's get a sense of all the different kinds of input components we can work with. +You can see a list of all the possible input components in the +[components gallery](/components/). + +In general, all the input components are imported with `from shiny.express import ui`, +and we can access each of the input components from the corresponding input +component function, `ui.input_*()`. +We typically pass in the input `id` as the first parameter, +and the `label` as the second parameter. +The `id` is a unique name for **each** input component that we can use +to look up its (reactive) value. +The `label` is the text that is displayed along with the input component, +it is usually the name or really short description for what the input component controls. +The rest of the arguments will differ for each component, +such as what values to be displayed for button choices, +or starting and ending range for a slider. +Each input component also has their own parameters for customizations specific for that particular input. + +:::{.callout-tip} +The [components gallery](/components/) is a great way to quickly see all the possible components +that come with Shiny for Python. +Each component page has their own mini example tutorial how to use the corresponding component. +It's useful to have the components page open to the side as you are +planning and building your application. +::: + +Let's combine a few of our layout knowledge from the previous lesson, +and add some input components to a shiny application. + +Here we have a fillable page with 2 columns, each containing a card +with a different UI component in it. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 300 +from shiny.express import ui + +ui.page_opts(fillable=True) + +with ui.layout_columns(): + with ui.card(): + ui.card_header("Card 1 header") + ui.p("Card 1 body") + ui.input_slider("slider", "Slider", 0, 10, 5) + + with ui.card(): + ui.card_header("Card 2 header") + ui.p("Card 2 body") + ui.input_text("text", "Add text", "") +``` + + + +:::{.callout-note .column-page-right} +## Exercise + +Now that you have a bit more practice with UIs and Input components, +Let's build add a few inputs to our existing tips dashboard. + +![](../../docs/assets/tipping-dashboard.png) + +Our application only has inputs in the left sidebar. +Let's add them to the application (we will work on connecting them with data later) + +1. [`input_slider()`](https://shiny.posit.co/py/components/inputs/slider-range/): + We'll use `0` and `100` as the lower and upper bounds for now. + When we load our data we can calculate actual data range. +2. [`input_checkbox_group()`](https://shiny.posit.co/py/components/inputs/checkbox-group/): + With a label of `Food service` and options for `Lunch` and `Dinner`. +3. [`input_action_button()`](https://shiny.posit.co/py/components/inputs/action-button/): + Labeled `Reset filter`. + +We will connect these inputs to outputs in the next lesson. + +For reference, here's our current code and application: + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 500 +from shiny.express import input, ui + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar (empty for now) +with ui.sidebar(open="desktop"): + "sidebar inputs" + +# body of application + +# first row of value boxes +with ui.layout_columns(fill=False): + with ui.value_box(): + "Total tippers" + "Value 1" + + with ui.value_box(): + "Average tip" + "Value 2" + + with ui.value_box(): + "Average bill" + "Value 3" + +# second row of cards +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Tips data") + "Tips DataFrame" + + with ui.card(full_screen=True): + ui.card_header("Total bill vs tip") + "Scatterplot" + +with ui.layout_columns(): + with ui.card(full_screen=True): + ui.card_header("Tip percentages") + "ridgeplot" + +``` + +::: + +::: {.callout-caution collapse="true" .column-page-right} + +## Solution + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 500 +from shiny.express import input, ui + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar +with ui.sidebar(open="desktop"): + ui.input_slider( + id="slider", + label="Bill amount", + min=0, + max=100, + value=[0, 100], + ) + ui.input_checkbox_group( + id="checkbox_group", + label="Food service", + choices={ + "Lunch": "Lunch", + "Dinner": "Dinner", + }, + selected=[ + "Lunch", + "Dinner", + ], + ) + ui.input_action_button("action_button", "Reset filter") + + +# body of application + +# first row of value boxes +with ui.layout_columns(fill=False): + with ui.value_box(): + "Total tippers" + "Value 1" + + with ui.value_box(): + "Average tip" + "Value 2" + + with ui.value_box(): + "Average bill" + "Value 3" + +# second row of cards +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Tips data") + "Tips DataFrame" + + with ui.card(full_screen=True): + ui.card_header("Total bill vs tip") + "Scatterplot" + +with ui.layout_columns(): + with ui.card(full_screen=True): + ui.card_header("Tip percentages") + "ridgeplot" + +``` +::: diff --git a/tutorials/intro-express/4-external.qmd b/tutorials/intro-express/4-external.qmd new file mode 100644 index 00000000..05b902ff --- /dev/null +++ b/tutorials/intro-express/4-external.qmd @@ -0,0 +1,196 @@ +--- +title: External Resources +--- + +It's good to keep the UI and code logic separate. +Since we have an outline of the application UI, +let's make sure we can get all the logic working for the code. + +Let's take a look at the application we're planning to make and make sure we +can code up all the individual parts first. + +![](../../docs/assets/tipping-dashboard.png) + +From the dashboard sketch we need to make the following outputs + +1. Load the tips data that can be filtered byt the bill amount and food service time +2. Display the tips data after filtering +3. Calculate the total number of tippers (i.e., number of rows after filtering) +4. Calculate average tip percentage after filtering +5. Calculate average bill after filtering +6. [Plotly scatterplot](https://plotly.com/python/line-and-scatter/) + comparing `tip` vs `total_bill` of the filtered data +7. [ridgeplot](https://ridgeplot.readthedocs.io/en/stable/) + comparing days of the week vs tip percentages of the filtered data + +Now that we can lay out components and have the output components react to the input components, +let's see how we can incorporate modules, packages, and external data into our application. + +Before we start, make sure you have pandas, plotly, and ridgeplot installed. +If you are following along this tutorial from the beginning, +make sure you are in the proper virtual environment + +::: {.panel-tabset} + +## pip + +```bash +pip install pandas plotly ridgeplot +``` + +## conda + +```bash +conda install -c conda-forge pandas plotly +pip install ridgeplot +``` + +## mamba + +```bash +mamba install install -c conda-forge pandas plotly +pip install ridgeplot +``` + +::: + + +## External Data + +External data can be read into a Shiny for Python just like any other +python data science project, e.g., pandas, polars, ibis, eager, duckdb, etc. + +:::{.callout-note} +You can use the Python `narwhals` library to convert between +different dataframe backends. + + +::: + +For example, if we wanted to read in data from the `tips.csv` file in pandas, +we can use the same code in our shiny for python application. + +```{python} +#| include: false + +import pandas as pd + +try: + tips = pd.read_csv("tips.csv") +except FileNotFoundError: + tips = pd.read_csv("tutorials/intro-express/tips.csv") +``` + +```python +import pandas as pd + +tips = pd.read_csv("tips.csv") +``` + +Next, let's create a few variables to serve as placeholders for the input components: + +```{python} +total_lower = tips.total_bill.min() +total_upper = tips.total_bill.max() +time_selected = tips.time.unique().tolist() +``` + +And a placeholder for the filtered tips dataframe: + +```{python} +idx1 = tips.total_bill.between( + left=total_lower, + right=total_upper, + inclusive="both", +) + +idx2 = tips.time.isin(time_selected) + +tips_filtered = tips[idx1 & idx2] +``` + +Now that we have a placeholder for the filtered dataframe, +we can write the code for the other components of the application. + +```{python} +tips_filtered.head() +``` + +## Individual values + +Now, let's calculate the individual numbers that are showed in the value boxes. + +```{python} +# total tippers +total_tippers = tips_filtered.shape[0] +total_tippers +``` + +```{python} +# average tip +perc = tips_filtered.tip / tips_filtered.total_bill +average_tip = f"{perc.mean():.1%}" +average_tip +``` + +```{python} +# average bill +bill = tips_filtered.total_bill.mean() +average_bill = f"${bill:.2f}" +average_bill +``` + +## Plots + +We now need to create 2 figures, a scatterplot and a ridgeplot. + +The scatterplot will use the `plotly` library + +```{python} +import plotly.express as px + +px.scatter( + tips_filtered, + x="total_bill", + y="tip", + trendline="lowess" +) +``` + +The ridgeplot will use the `ridgeplot` library + +```{python} +from ridgeplot import ridgeplot + +tips_filtered["percent"] = tips_filtered.tip / tips_filtered.total_bill + +uvals = tips_filtered.day.unique() +samples = [[tips_filtered.percent[tips_filtered.day == val]] for val in uvals] + +plt = ridgeplot( + samples=samples, + labels=uvals, + bandwidth=0.01, + colorscale="viridis", + colormode="row-index" +) + +plt.update_layout( + legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="center", + x=0.5 + ) +) + +plt +``` + +## Next steps + +Now we have the working code for all the parts of our application. +Next we will add these outputs to the application +and then link the input components to our placeholder variables +to filtered the data. diff --git a/tutorials/intro-express/4-external.quarto_ipynb b/tutorials/intro-express/4-external.quarto_ipynb new file mode 100644 index 00000000..96ef7a3f --- /dev/null +++ b/tutorials/intro-express/4-external.quarto_ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: External Resources\n", + "---\n", + "\n", + "\n", + "It's good to keep the UI and code logic separate.\n", + "Since we have an outline of the application UI,\n", + "let's make sure we can get all the logic working for the code.\n", + "\n", + "Let's take a look at the application we're planning to make and make sure we\n", + "can code up all the individual parts first.\n", + "\n", + "![](../../docs/assets/tipping-dashboard.png)\n", + "\n", + "From the dashboard sketch we need to make the following outputs\n", + "\n", + "1. Load the tips data that can be filtered byt the bill amount and food service time\n", + "2. Display the tips data after filtering\n", + "3. Calculate the total number of tippers (i.e., number of rows after filtering)\n", + "4. Calculate average tip percentage after filtering\n", + "5. Calculate average bill after filtering\n", + "6. [Plotly scatterplot](https://plotly.com/python/line-and-scatter/)\n", + " comparing `tip` vs `total_bill` of the filtered data\n", + "7. [ridgeplot](https://ridgeplot.readthedocs.io/en/stable/)\n", + " comparing days of the week vs tip percentages of the filtered data\n", + "\n", + "Now that we can lay out components and have the output components react to the input components,\n", + "let's see how we can incorporate modules, packages, and external data into our application.\n", + "\n", + "Before we start, make sure you have pandas, plotly, and ridgeplot installed.\n", + "If you are following along this tutorial from the beginning,\n", + "make sure you are in the proper virtual environment\n", + "\n", + "::: {.panel-tabset}\n", + "\n", + "## pip\n", + "\n", + "```bash\n", + "pip install pandas plotly ridgeplot\n", + "```\n", + "\n", + "## conda\n", + "\n", + "```bash\n", + "conda install -c conda-forge pandas plotly\n", + "pip install ridgeplot\n", + "```\n", + "\n", + "## mamba\n", + "\n", + "```bash\n", + "mamba install install -c conda-forge pandas plotly\n", + "pip install ridgeplot\n", + "```\n", + "\n", + ":::\n", + "\n", + "\n", + "## External Data\n", + "\n", + "External data can be read into a Shiny for Python just like any other\n", + "python data science project, e.g., pandas, polars, ibis, eager, duckdb, etc.\n", + "\n", + ":::{.callout-note}\n", + "You can use the Python `narwhals` library to convert between\n", + "different dataframe backends.\n", + "\n", + "\n", + ":::\n", + "\n", + "For example, if we wanted to read in data from the `tips.csv` file in pandas,\n", + "we can use the same code in our shiny for python application.\n" + ], + "id": "7f9cb307" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "#| include: false\n", + "\n", + "import pandas as pd\n", + "\n", + "try:\n", + " tips = pd.read_csv(\"tips.csv\")\n", + "except FileNotFoundError:\n", + " tips = pd.read_csv(\"tutorials/intro-express/tips.csv\")" + ], + "id": "4e4d9bd6", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```python\n", + "import pandas as pd\n", + "\n", + "tips = pd.read_csv(\"tips.csv\")\n", + "```\n", + "\n", + "Next, let's create a few variables to serve as placeholders for the input components:\n" + ], + "id": "3c84c3b8" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "total_lower = tips.total_bill.min()\n", + "total_upper = tips.total_bill.max()\n", + "time_selected = tips.time.unique().tolist()" + ], + "id": "69757743", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And a placeholder for the filtered tips dataframe:\n" + ], + "id": "ad67bf6d" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "idx1 = tips.total_bill.between(\n", + " left=total_lower,\n", + " right=total_upper,\n", + " inclusive=\"both\",\n", + ")\n", + "\n", + "idx2 = tips.time.isin(time_selected)\n", + "\n", + "tips_filtered = tips[idx1 & idx2]" + ], + "id": "97d5101d", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have a placeholder for the filtered dataframe,\n", + "we can write the code for the other components of the application.\n" + ], + "id": "725aac84" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "tips_filtered.head()" + ], + "id": "76e30ca9", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Individual values\n", + "\n", + "Now, let's calculate the individual numbers that are showed in the value boxes.\n" + ], + "id": "7b502085" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# total tippers\n", + "total_tippers = tips_filtered.shape[0]\n", + "total_tippers" + ], + "id": "a25ff9fd", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# average tip\n", + "perc = tips_filtered.tip / tips_filtered.total_bill\n", + "average_tip = f\"{perc.mean():.1%}\"\n", + "average_tip" + ], + "id": "74b10c7b", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# average bill\n", + "bill = tips_filtered.total_bill.mean()\n", + "average_bill = f\"${bill:.2f}\"\n", + "average_bill" + ], + "id": "da8bdf28", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plots\n", + "\n", + "We now need to create 2 figures, a scatterplot and a ridgeplot.\n", + "\n", + "The scatterplot will use the `plotly` library\n" + ], + "id": "05e2fc36" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import plotly.express as px\n", + "\n", + "px.scatter(\n", + " tips_filtered,\n", + " x=\"total_bill\",\n", + " y=\"tip\",\n", + " trendline=\"lowess\"\n", + ")" + ], + "id": "5f52cf0d", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ridgeplot will use the `ridgeplot` library\n" + ], + "id": "39a291a9" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from ridgeplot import ridgeplot\n", + "\n", + "tips_filtered[\"percent\"] = tips_filtered.tip / tips_filtered.total_bill\n", + "\n", + "uvals = tips_filtered.day.unique()\n", + "samples = [[tips_filtered.percent[tips_filtered.day == val]] for val in uvals]\n", + "\n", + "plt = ridgeplot(\n", + " samples=samples,\n", + " labels=uvals,\n", + " bandwidth=0.01,\n", + " colorscale=\"viridis\",\n", + " colormode=\"row-index\"\n", + ")\n", + "\n", + "plt.update_layout(\n", + " legend=dict(\n", + " orientation=\"h\",\n", + " yanchor=\"bottom\",\n", + " y=1.02,\n", + " xanchor=\"center\",\n", + " x=0.5\n", + " )\n", + ")\n", + "\n", + "plt" + ], + "id": "7c1e3637", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Now we have the working code for all the parts of our application.\n", + "Next we will add these outputs to the application\n", + "and then link the input components to our placeholder variables\n", + "to filtered the data." + ], + "id": "0b68a24b" + } + ], + "metadata": { + "kernelspec": { + "name": "pfe_book", + "language": "python", + "display_name": "pfe_book", + "path": "/Users/danielchen/Library/Jupyter/kernels/pfe_book" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/tutorials/intro-express/5-outputs.qmd b/tutorials/intro-express/5-outputs.qmd new file mode 100644 index 00000000..d368ec3f --- /dev/null +++ b/tutorials/intro-express/5-outputs.qmd @@ -0,0 +1,793 @@ +--- +title: Output Components +--- + + + +Now that we know how to lay our the application and insert input components for the user to interact with, +let's create some output components that **react** to the input components. + +Output components all begin with a function with a `ui.render_*` decorator above a function definition. +The decorator is one of the ways that you signal to Shiny that the code will react to some change +in the application. +The name of the function does not matter to shiny, but you should pick a name +that hints at what value is going to be returned. +Finally, the body of the function should return the corresponding object for the output component. +Again, the decorator function signals to Shiny what kind of output component is displayed in the application. + +In Shiny Express, wherever the output function is defined +(i.e., where we are using the `ui.render_*` decorator), +is where the output will be displayed. +So it's perfectly normal to see function definitions throughout the application. + +:::{.callout-important} +In Shiny Express, +the name of the function is used as the output ID. +Each function that is decorated with a `@rander.*` decorator should have a unique name. +::: + + +The `@render.text` output is one way you can help debug your application visually. +Similar to `print()` statement debugging, +except the print statement will be rendered in your application. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 150 +from shiny.express import input, render, ui + +ui.input_slider("val", "Slider label", min=0, max=100, value=50) + +@render.text +def slider_val(): + return f"Slider value: {input.val()}" +``` + +:::{.callout-note} +Since we are now using output components, you will need to also +import the `render` module from `shiny.express` + + +```python +from shiny.express import input, render, ui +``` +::: + + +## Use input values + +In the application above, we had this particular line in our function body, `input.var()`. +This line shows one of the main features in Shiny, reactive values. + +- The `input` variable automatically holds all the values from the input components as a Python dictionary +- We can access the input component value with dot notation and + use the same `id` we defined in the `ui.input_*()` function +- The `input.var` represents the actual reactive value object, + if we want to actually calculate the current reactive value, + we need to call it as a function with `input.var()` + + + +:::{.callout-tip .column-page-right} +## Exercise + + +Let's add `@render.text` outputs to the sidebar so we can confirm what the code will see from the input components. +We will work with the slider and checkbox components. + +1. Our current application should already have `input_slider()` and `input_checkbox_group()` components +2. Define separate output functions in the sidebar under the reset button, + one for the `input_sider()` values, and another for the `input_checkbox_group()` values. + - Use the `input.()` pattern to have shiny reactively get the input component values + - Return the value you want to use in the application (returning the input values directly is fine) +3. Decorate the functions with `@render.text` to signal that we want the returned value rendered as text in the application +4. Add 2 additional text outputs, one for the lower bound of the input slider, + and another for the upper bound of the input slider + +:::{.callout-tip} +The `ui.input_slider()` component returns a list of values +where the first (`0` index) is the lower slider value, +and the second (`1` index) is the upper slider value. +::: + +Here is our current application for reference: + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 500 +from shiny.express import input, ui + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar +with ui.sidebar(open="desktop"): + ui.input_slider( + id="slider", + label="Bill amount", + min=0, + max=100, + value=[0, 100], + ) + ui.input_checkbox_group( + id="checkbox_group", + label="Food service", + choices={ + "Lunch": "Lunch", + "Dinner": "Dinner", + }, + selected=[ + "Lunch", + "Dinner", + ], + ) + ui.input_action_button("action_button", "Reset filter") + + + +# body of application + +# first row of value boxes +with ui.layout_columns(fill=False): + with ui.value_box(): + "Total tippers" + "Value 1" + + with ui.value_box(): + "Average tip" + "Value 2" + + with ui.value_box(): + "Average bill" + "Value 3" + +# second row of cards +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Tips data") + "Tips DataFrame" + + with ui.card(full_screen=True): + ui.card_header("Total bill vs tip") + "Scatterplot" + +with ui.layout_columns(): + with ui.card(full_screen=True): + ui.card_header("Tip percentages") + "ridgeplot" + +``` +::: + + +::: {.callout-caution collapse="true" .column-page-right} + +## Solution + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 500 +from shiny.express import input, ui, render + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar +with ui.sidebar(open="desktop"): + ui.input_slider( + id="slider", + label="Bill amount", + min=0, + max=100, + value=[0, 100], + ) + ui.input_checkbox_group( + id="checkbox_group", + label="Food service", + choices={ + "Lunch": "Lunch", + "Dinner": "Dinner", + }, + selected=[ + "Lunch", + "Dinner", + ], + ) + ui.input_action_button("action_button", "Reset filter") + + + @render.text + def slider_val(): + return f"Slider values: {input.slider()}" + + @render.text + def checkbox_group_val(): + return f"Checkbox values: {input.checkbox_group()}" + + @render.text + def slider_val_0(): + return f"Slider value (lower): {input.slider()[0]}" + + @render.text + def slider_val_1(): + return f"Slider value (upper): {input.slider()[1]}" + + +# body of application + +# first row of value boxes +with ui.layout_columns(fill=False): + with ui.value_box(): + "Total tippers" + "Value 1" + + with ui.value_box(): + "Average tip" + "Value 2" + + with ui.value_box(): + "Average bill" + "Value 3" + +# second row of cards +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Tips data") + "Tips DataFrame" + + with ui.card(full_screen=True): + ui.card_header("Total bill vs tip") + "Scatterplot" + +with ui.layout_columns(): + with ui.card(full_screen=True): + ui.card_header("Tip percentages") + "ridgeplot" + +``` +::: + +In the previous lesson, +we created variables that served as place holders we can use to filter our `tips` data + +```python +total_lower = tips.total_bill.min() +total_upper = tips.total_bill.max() +time_selected = tips.time.unique().tolist() +``` + +Instead of these hardcoded values, +we can use the values from the `input_*()` components instead. + +:::{.callout-tip} +## Exercise + +We will learn more about reactivity and reactive calculations in the next lesson. +But in this exercise, +we will use the input component values to filter our tips dataframe for each +output component. + +For reference, below we have the code for each of the outputs we created earlier. +Wrap each output into a function and decorate it with the corresponding +output component decorator. + +- dataframe: [`render.data_frame`](https://shiny.posit.co/py/api/express/express.render.data_frame.html) +- value box text: [`render.express`](https://shiny.posit.co/py/api/express/express.render.express.html) +- scatterplot (plotly): [`render_plotly`](https://shiny.posit.co/py/docs/jupyter-widgets.html) +- ridgeplot (plotly): [`render_plotly`](https://shiny.posit.co/py/docs/jupyter-widgets.html) + +:::::: {.panel-tabset} + +## dataframe + +```python +tips = pd.read_csv("tips.csv") + +total_lower = tips.total_bill.min() +total_upper = tips.total_bill.max() +time_selected = tips.time.unique().tolist() + +idx1 = tips.total_bill.between( + left=total_lower, + right=total_upper, + inclusive="both", +) + +idx2 = tips.time.isin(time_selected) + +tips_filtered = tips[idx1 & idx2] +``` + +## valuebox + +```python +# total tippers +total_tippers = tips_filtered.shape[0] + +# average tip +perc = tips_filtered.tip / tips_filtered.total_bill +average_tip = f"{perc.mean():.1%}" + +# average bill +bill = tips_filtered.total_bill.mean() +average_bill = f"${bill:.2f}" +``` + +## scatterplot + +```python +# scatterplot +import plotly.express as px + +px.scatter( + tips_filtered, + x="total_bill", + y="tip", + trendline="lowess" +) +``` + +## ridgeplot + +```python +# ridgeplot +from ridgeplot import ridgeplot + +tips_filtered["percent"] = tips_filtered.tip / tips_filtered.total_bill + +uvals = tips_filtered.day.unique() +samples = [[tips_filtered.percent[tips_filtered.day == val]] for val in uvals] + +plt = ridgeplot( + samples=samples, + labels=uvals, + bandwidth=0.01, + colorscale="viridis", + colormode="row-index" +) + +plt.update_layout( + legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="center", + x=0.5 + ) +) +) +``` +:::::: + +::: + +::: {.callout-caution collapse="true" .column-page-right} +## Solution + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 600 + +from pathlib import Path # only used for shinylive render + +import pandas as pd +import plotly.express as px +from ridgeplot import ridgeplot +from shiny.express import input, render, ui +from shinywidgets import render_plotly + +# only used for shinylive render +file = Path(__file__).parent / "tips.csv" + +# you may need to change the path to the tips.csv file +tips = pd.read_csv(file) + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar +with ui.sidebar(open="desktop"): + ui.input_slider( + id="slider", + label="Bill amount", + min=tips.total_bill.min(), + max=tips.total_bill.max(), + value=[tips.total_bill.min(), tips.total_bill.max()]) + ui.input_checkbox_group( + id="checkbox_group", + label="Food service", + choices={ + "Lunch": "Lunch", + "Dinner": "Dinner", + }, + selected=[ + "Lunch", + "Dinner", + ], + ) + ui.input_action_button("action_button", "Reset filter") + + +# body of application + +# first row of value boxes +with ui.layout_columns(fill=False): + with ui.value_box(): + "Total tippers" + @render.express + def total_tippers(): + idx1 = tips.total_bill.between( + left=input.slider()[0], + right=input.slider()[1], + inclusive="both", + ) + idx2 = tips.time.isin(input.checkbox_group()) + tips_filtered = tips[idx1 & idx2] + + tips_filtered.shape[0] + + with ui.value_box(): + "Average tip" + @render.express + def average_tip(): + idx1 = tips.total_bill.between( + left=input.slider()[0], + right=input.slider()[1], + inclusive="both", + ) + idx2 = tips.time.isin(input.checkbox_group()) + tips_filtered = tips[idx1 & idx2] + + perc = tips_filtered.tip / tips_filtered.total_bill + f"{perc.mean():.1%}" + + with ui.value_box(): + "Average bill" + @render.express + def average_bill(): + idx1 = tips.total_bill.between( + left=input.slider()[0], + right=input.slider()[1], + inclusive="both", + ) + idx2 = tips.time.isin(input.checkbox_group()) + tips_filtered = tips[idx1 & idx2] + + bill = tips_filtered.total_bill.mean() + f"${bill:.2f}" + +# second row of cards +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Tips data") + @render.data_frame + def table(): + idx1 = tips.total_bill.between( + left=input.slider()[0], + right=input.slider()[1], + inclusive="both", + ) + idx2 = tips.time.isin(input.checkbox_group()) + tips_filtered = tips[idx1 & idx2] + + return render.DataGrid(tips_filtered) + + with ui.card(full_screen=True): + ui.card_header("Total bill vs tip") + @render_plotly + def scatterplot(): + idx1 = tips.total_bill.between( + left=input.slider()[0], + right=input.slider()[1], + inclusive="both", + ) + idx2 = tips.time.isin(input.checkbox_group()) + tips_filtered = tips[idx1 & idx2] + + return px.scatter( + tips_filtered, + x="total_bill", + y="tip", + trendline="lowess", + ) + +with ui.layout_columns(): + with ui.card(full_screen=True): + ui.card_header("Tip percentages") + @render_plotly + def tip_perc(): + idx1 = tips.total_bill.between( + left=input.slider()[0], + right=input.slider()[1], + inclusive="both", + ) + idx2 = tips.time.isin(input.checkbox_group()) + tips_filtered = tips[idx1 & idx2] + + tips_filtered["percent"] = tips_filtered.tip / tips_filtered.total_bill + + uvals = tips_filtered.day.unique() + samples = [[tips_filtered.percent[tips_filtered.day == val]] for val in uvals] + + plt = ridgeplot( + samples=samples, + labels=uvals, + bandwidth=0.01, + colorscale="viridis", + colormode="row-index" + ) + + plt.update_layout( + legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="center", + x=0.5 + ) + ) + return plt + +## file: requirements.txt +ridgeplot==0.1.25 + +## file: tips.csv +total_bill,tip,sex,smoker,day,time,size +16.99,1.01,Female,No,Sun,Dinner,2 +10.34,1.66,Male,No,Sun,Dinner,3 +21.01,3.5,Male,No,Sun,Dinner,3 +23.68,3.31,Male,No,Sun,Dinner,2 +24.59,3.61,Female,No,Sun,Dinner,4 +25.29,4.71,Male,No,Sun,Dinner,4 +8.77,2.0,Male,No,Sun,Dinner,2 +26.88,3.12,Male,No,Sun,Dinner,4 +15.04,1.96,Male,No,Sun,Dinner,2 +14.78,3.23,Male,No,Sun,Dinner,2 +10.27,1.71,Male,No,Sun,Dinner,2 +35.26,5.0,Female,No,Sun,Dinner,4 +15.42,1.57,Male,No,Sun,Dinner,2 +18.43,3.0,Male,No,Sun,Dinner,4 +14.83,3.02,Female,No,Sun,Dinner,2 +21.58,3.92,Male,No,Sun,Dinner,2 +10.33,1.67,Female,No,Sun,Dinner,3 +16.29,3.71,Male,No,Sun,Dinner,3 +16.97,3.5,Female,No,Sun,Dinner,3 +20.65,3.35,Male,No,Sat,Dinner,3 +17.92,4.08,Male,No,Sat,Dinner,2 +20.29,2.75,Female,No,Sat,Dinner,2 +15.77,2.23,Female,No,Sat,Dinner,2 +39.42,7.58,Male,No,Sat,Dinner,4 +19.82,3.18,Male,No,Sat,Dinner,2 +17.81,2.34,Male,No,Sat,Dinner,4 +13.37,2.0,Male,No,Sat,Dinner,2 +12.69,2.0,Male,No,Sat,Dinner,2 +21.7,4.3,Male,No,Sat,Dinner,2 +19.65,3.0,Female,No,Sat,Dinner,2 +9.55,1.45,Male,No,Sat,Dinner,2 +18.35,2.5,Male,No,Sat,Dinner,4 +15.06,3.0,Female,No,Sat,Dinner,2 +20.69,2.45,Female,No,Sat,Dinner,4 +17.78,3.27,Male,No,Sat,Dinner,2 +24.06,3.6,Male,No,Sat,Dinner,3 +16.31,2.0,Male,No,Sat,Dinner,3 +16.93,3.07,Female,No,Sat,Dinner,3 +18.69,2.31,Male,No,Sat,Dinner,3 +31.27,5.0,Male,No,Sat,Dinner,3 +16.04,2.24,Male,No,Sat,Dinner,3 +17.46,2.54,Male,No,Sun,Dinner,2 +13.94,3.06,Male,No,Sun,Dinner,2 +9.68,1.32,Male,No,Sun,Dinner,2 +30.4,5.6,Male,No,Sun,Dinner,4 +18.29,3.0,Male,No,Sun,Dinner,2 +22.23,5.0,Male,No,Sun,Dinner,2 +32.4,6.0,Male,No,Sun,Dinner,4 +28.55,2.05,Male,No,Sun,Dinner,3 +18.04,3.0,Male,No,Sun,Dinner,2 +12.54,2.5,Male,No,Sun,Dinner,2 +10.29,2.6,Female,No,Sun,Dinner,2 +34.81,5.2,Female,No,Sun,Dinner,4 +9.94,1.56,Male,No,Sun,Dinner,2 +25.56,4.34,Male,No,Sun,Dinner,4 +19.49,3.51,Male,No,Sun,Dinner,2 +38.01,3.0,Male,Yes,Sat,Dinner,4 +26.41,1.5,Female,No,Sat,Dinner,2 +11.24,1.76,Male,Yes,Sat,Dinner,2 +48.27,6.73,Male,No,Sat,Dinner,4 +20.29,3.21,Male,Yes,Sat,Dinner,2 +13.81,2.0,Male,Yes,Sat,Dinner,2 +11.02,1.98,Male,Yes,Sat,Dinner,2 +18.29,3.76,Male,Yes,Sat,Dinner,4 +17.59,2.64,Male,No,Sat,Dinner,3 +20.08,3.15,Male,No,Sat,Dinner,3 +16.45,2.47,Female,No,Sat,Dinner,2 +3.07,1.0,Female,Yes,Sat,Dinner,1 +20.23,2.01,Male,No,Sat,Dinner,2 +15.01,2.09,Male,Yes,Sat,Dinner,2 +12.02,1.97,Male,No,Sat,Dinner,2 +17.07,3.0,Female,No,Sat,Dinner,3 +26.86,3.14,Female,Yes,Sat,Dinner,2 +25.28,5.0,Female,Yes,Sat,Dinner,2 +14.73,2.2,Female,No,Sat,Dinner,2 +10.51,1.25,Male,No,Sat,Dinner,2 +17.92,3.08,Male,Yes,Sat,Dinner,2 +27.2,4.0,Male,No,Thur,Lunch,4 +22.76,3.0,Male,No,Thur,Lunch,2 +17.29,2.71,Male,No,Thur,Lunch,2 +19.44,3.0,Male,Yes,Thur,Lunch,2 +16.66,3.4,Male,No,Thur,Lunch,2 +10.07,1.83,Female,No,Thur,Lunch,1 +32.68,5.0,Male,Yes,Thur,Lunch,2 +15.98,2.03,Male,No,Thur,Lunch,2 +34.83,5.17,Female,No,Thur,Lunch,4 +13.03,2.0,Male,No,Thur,Lunch,2 +18.28,4.0,Male,No,Thur,Lunch,2 +24.71,5.85,Male,No,Thur,Lunch,2 +21.16,3.0,Male,No,Thur,Lunch,2 +28.97,3.0,Male,Yes,Fri,Dinner,2 +22.49,3.5,Male,No,Fri,Dinner,2 +5.75,1.0,Female,Yes,Fri,Dinner,2 +16.32,4.3,Female,Yes,Fri,Dinner,2 +22.75,3.25,Female,No,Fri,Dinner,2 +40.17,4.73,Male,Yes,Fri,Dinner,4 +27.28,4.0,Male,Yes,Fri,Dinner,2 +12.03,1.5,Male,Yes,Fri,Dinner,2 +21.01,3.0,Male,Yes,Fri,Dinner,2 +12.46,1.5,Male,No,Fri,Dinner,2 +11.35,2.5,Female,Yes,Fri,Dinner,2 +15.38,3.0,Female,Yes,Fri,Dinner,2 +44.3,2.5,Female,Yes,Sat,Dinner,3 +22.42,3.48,Female,Yes,Sat,Dinner,2 +20.92,4.08,Female,No,Sat,Dinner,2 +15.36,1.64,Male,Yes,Sat,Dinner,2 +20.49,4.06,Male,Yes,Sat,Dinner,2 +25.21,4.29,Male,Yes,Sat,Dinner,2 +18.24,3.76,Male,No,Sat,Dinner,2 +14.31,4.0,Female,Yes,Sat,Dinner,2 +14.0,3.0,Male,No,Sat,Dinner,2 +7.25,1.0,Female,No,Sat,Dinner,1 +38.07,4.0,Male,No,Sun,Dinner,3 +23.95,2.55,Male,No,Sun,Dinner,2 +25.71,4.0,Female,No,Sun,Dinner,3 +17.31,3.5,Female,No,Sun,Dinner,2 +29.93,5.07,Male,No,Sun,Dinner,4 +10.65,1.5,Female,No,Thur,Lunch,2 +12.43,1.8,Female,No,Thur,Lunch,2 +24.08,2.92,Female,No,Thur,Lunch,4 +11.69,2.31,Male,No,Thur,Lunch,2 +13.42,1.68,Female,No,Thur,Lunch,2 +14.26,2.5,Male,No,Thur,Lunch,2 +15.95,2.0,Male,No,Thur,Lunch,2 +12.48,2.52,Female,No,Thur,Lunch,2 +29.8,4.2,Female,No,Thur,Lunch,6 +8.52,1.48,Male,No,Thur,Lunch,2 +14.52,2.0,Female,No,Thur,Lunch,2 +11.38,2.0,Female,No,Thur,Lunch,2 +22.82,2.18,Male,No,Thur,Lunch,3 +19.08,1.5,Male,No,Thur,Lunch,2 +20.27,2.83,Female,No,Thur,Lunch,2 +11.17,1.5,Female,No,Thur,Lunch,2 +12.26,2.0,Female,No,Thur,Lunch,2 +18.26,3.25,Female,No,Thur,Lunch,2 +8.51,1.25,Female,No,Thur,Lunch,2 +10.33,2.0,Female,No,Thur,Lunch,2 +14.15,2.0,Female,No,Thur,Lunch,2 +16.0,2.0,Male,Yes,Thur,Lunch,2 +13.16,2.75,Female,No,Thur,Lunch,2 +17.47,3.5,Female,No,Thur,Lunch,2 +34.3,6.7,Male,No,Thur,Lunch,6 +41.19,5.0,Male,No,Thur,Lunch,5 +27.05,5.0,Female,No,Thur,Lunch,6 +16.43,2.3,Female,No,Thur,Lunch,2 +8.35,1.5,Female,No,Thur,Lunch,2 +18.64,1.36,Female,No,Thur,Lunch,3 +11.87,1.63,Female,No,Thur,Lunch,2 +9.78,1.73,Male,No,Thur,Lunch,2 +7.51,2.0,Male,No,Thur,Lunch,2 +14.07,2.5,Male,No,Sun,Dinner,2 +13.13,2.0,Male,No,Sun,Dinner,2 +17.26,2.74,Male,No,Sun,Dinner,3 +24.55,2.0,Male,No,Sun,Dinner,4 +19.77,2.0,Male,No,Sun,Dinner,4 +29.85,5.14,Female,No,Sun,Dinner,5 +48.17,5.0,Male,No,Sun,Dinner,6 +25.0,3.75,Female,No,Sun,Dinner,4 +13.39,2.61,Female,No,Sun,Dinner,2 +16.49,2.0,Male,No,Sun,Dinner,4 +21.5,3.5,Male,No,Sun,Dinner,4 +12.66,2.5,Male,No,Sun,Dinner,2 +16.21,2.0,Female,No,Sun,Dinner,3 +13.81,2.0,Male,No,Sun,Dinner,2 +17.51,3.0,Female,Yes,Sun,Dinner,2 +24.52,3.48,Male,No,Sun,Dinner,3 +20.76,2.24,Male,No,Sun,Dinner,2 +31.71,4.5,Male,No,Sun,Dinner,4 +10.59,1.61,Female,Yes,Sat,Dinner,2 +10.63,2.0,Female,Yes,Sat,Dinner,2 +50.81,10.0,Male,Yes,Sat,Dinner,3 +15.81,3.16,Male,Yes,Sat,Dinner,2 +7.25,5.15,Male,Yes,Sun,Dinner,2 +31.85,3.18,Male,Yes,Sun,Dinner,2 +16.82,4.0,Male,Yes,Sun,Dinner,2 +32.9,3.11,Male,Yes,Sun,Dinner,2 +17.89,2.0,Male,Yes,Sun,Dinner,2 +14.48,2.0,Male,Yes,Sun,Dinner,2 +9.6,4.0,Female,Yes,Sun,Dinner,2 +34.63,3.55,Male,Yes,Sun,Dinner,2 +34.65,3.68,Male,Yes,Sun,Dinner,4 +23.33,5.65,Male,Yes,Sun,Dinner,2 +45.35,3.5,Male,Yes,Sun,Dinner,3 +23.17,6.5,Male,Yes,Sun,Dinner,4 +40.55,3.0,Male,Yes,Sun,Dinner,2 +20.69,5.0,Male,No,Sun,Dinner,5 +20.9,3.5,Female,Yes,Sun,Dinner,3 +30.46,2.0,Male,Yes,Sun,Dinner,5 +18.15,3.5,Female,Yes,Sun,Dinner,3 +23.1,4.0,Male,Yes,Sun,Dinner,3 +15.69,1.5,Male,Yes,Sun,Dinner,2 +19.81,4.19,Female,Yes,Thur,Lunch,2 +28.44,2.56,Male,Yes,Thur,Lunch,2 +15.48,2.02,Male,Yes,Thur,Lunch,2 +16.58,4.0,Male,Yes,Thur,Lunch,2 +7.56,1.44,Male,No,Thur,Lunch,2 +10.34,2.0,Male,Yes,Thur,Lunch,2 +43.11,5.0,Female,Yes,Thur,Lunch,4 +13.0,2.0,Female,Yes,Thur,Lunch,2 +13.51,2.0,Male,Yes,Thur,Lunch,2 +18.71,4.0,Male,Yes,Thur,Lunch,3 +12.74,2.01,Female,Yes,Thur,Lunch,2 +13.0,2.0,Female,Yes,Thur,Lunch,2 +16.4,2.5,Female,Yes,Thur,Lunch,2 +20.53,4.0,Male,Yes,Thur,Lunch,4 +16.47,3.23,Female,Yes,Thur,Lunch,3 +26.59,3.41,Male,Yes,Sat,Dinner,3 +38.73,3.0,Male,Yes,Sat,Dinner,4 +24.27,2.03,Male,Yes,Sat,Dinner,2 +12.76,2.23,Female,Yes,Sat,Dinner,2 +30.06,2.0,Male,Yes,Sat,Dinner,3 +25.89,5.16,Male,Yes,Sat,Dinner,4 +48.33,9.0,Male,No,Sat,Dinner,4 +13.27,2.5,Female,Yes,Sat,Dinner,2 +28.17,6.5,Female,Yes,Sat,Dinner,3 +12.9,1.1,Female,Yes,Sat,Dinner,2 +28.15,3.0,Male,Yes,Sat,Dinner,5 +11.59,1.5,Male,Yes,Sat,Dinner,2 +7.74,1.44,Male,Yes,Sat,Dinner,2 +30.14,3.09,Female,Yes,Sat,Dinner,4 +12.16,2.2,Male,Yes,Fri,Lunch,2 +13.42,3.48,Female,Yes,Fri,Lunch,2 +8.58,1.92,Male,Yes,Fri,Lunch,1 +15.98,3.0,Female,No,Fri,Lunch,3 +13.42,1.58,Male,Yes,Fri,Lunch,2 +16.27,2.5,Female,Yes,Fri,Lunch,2 +10.09,2.0,Female,Yes,Fri,Lunch,2 +20.45,3.0,Male,No,Sat,Dinner,4 +13.28,2.72,Male,No,Sat,Dinner,2 +22.12,2.88,Female,Yes,Sat,Dinner,2 +24.01,2.0,Male,Yes,Sat,Dinner,4 +15.69,3.0,Male,Yes,Sat,Dinner,3 +11.61,3.39,Male,No,Sat,Dinner,2 +10.77,1.47,Male,No,Sat,Dinner,2 +15.53,3.0,Male,Yes,Sat,Dinner,2 +10.07,1.25,Male,No,Sat,Dinner,2 +12.6,1.0,Male,Yes,Sat,Dinner,2 +32.83,1.17,Male,Yes,Sat,Dinner,2 +35.83,4.67,Female,No,Sat,Dinner,3 +29.03,5.92,Male,No,Sat,Dinner,3 +27.18,2.0,Female,Yes,Sat,Dinner,2 +22.67,2.0,Male,Yes,Sat,Dinner,2 +17.82,1.75,Male,No,Sat,Dinner,2 +18.78,3.0,Female,No,Thur,Dinner,2 + +``` +::: diff --git a/tutorials/intro-express/6-reactivity.qmd b/tutorials/intro-express/6-reactivity.qmd new file mode 100644 index 00000000..0d8654de --- /dev/null +++ b/tutorials/intro-express/6-reactivity.qmd @@ -0,0 +1,143 @@ +--- +title: Reactivity +--- + +We've been using the term "reactive" a lot during these tutorials. +But what does it mean? +It's actually more than "user interacts with input and new value gets calculated". +Reactivity is actually what makes Shiny special: +when an input changes, only the minimum amount of calculations are made to update the outputs. +This makes shiny very efficient. + +Shiny knows to re-execute **reactive functions** (e.g., `render` functions) when their **reactive dependencies** (e.g., `input`) change. +There are other main forms of reactive functions and dependencies: + +* Calculations with `@reactive.calc` + * Write your reactive calculation once, then call it as needed. +* Side effects with `@reactive.effect` + * Effects are similar to `@render.*` functions, but they don't return anything. They're used for their side-effects (e.g., writing to a database, sending an email, etc.) +* Reactive values with `reactive.value` + * Create `input`-like values that aren't tied to input controls and can be updated. They're often used to maintain state in an app. + +In this lesson we'll focus on the `@reactive.calc`, let's see why we may want reactive calculations. + +```{mermaid} +flowchart LR + A[Input] --> B(Calculated from Input) + B --> C{Value calculated from the input calc} + B --> D[Another value calculated from the input calc] +``` + +Let's say we have an input (A), +this input creates a value in the application (B). +But what if another part of the application needs to use this calculated value (C) +or another part (D)? + +Similar to why we create variables in python to capture intermediate values, +we can save these intermediate "reactive" calculations in shiny. + + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 200 + +from shiny import reactive +from shiny.express import input, render, ui + +ui.input_slider("x", "Slider value", min=0, max=100, value=10) + +# we need to make a calculation from an input value +@render.text +def x_squared_text(): + return f"Squared value: {input.x() ** 2}" + +# we can save this calculation to be used later +@reactive.calc +def x_squared(): + return input.x() ** 2 + +# we can use that saved calculation +@render.text +def x_squared_calc_text(): + return f"Saved squared: {x_squared()}" + +# we can build on top of that saved calculation +@render.text +def x_squared_half_calc_text(): + return f"Build on squared value: {x_squared() / 2}" + +# we don't need to re-calculate everything from the input again +@render.text +def x_squared_half_text(): + return f"Recalculate from input: {input.x() ** 2 / 2}" +``` + +The app above shows that we only need to make the `input.x() ** 2` calculation **once**, +so we do not need to repeat that calculation, `input.x() ** 2 / 2`. + +This idea is really similar to the following Python code, +where we save an intermediate value, +instead of repeating a calculation. + +```{python} +initial_input_value = 3 + +initial_squared = initial_input_value ** 2 + +initial_squared_half = initial_squared / 2 +``` + +If we did not save the `initial_squared` intermediate value, +we would have to re-make that calculation when doing a square and half. + +```{python} +initial_squared_half = initial_input_value ** 2 / 2 +``` + + + +Currently our application is re-filtering the data from the input components +for each output component displayed in the application. + +```{mermaid} +flowchart LR + A[Input Slider] --> C(Filtered Data) + B[Input Checkboxes] --> C + C --> D{value box 1} + + E[Input Slider] --> G(Filtered Data) + F[Input Checkboxes] --> G + G --> H{value box 2} + + I[Input Slider] --> K(Filtered Data) + J[Input Checkboxes] --> K + K --> L{value box 3} + + M[Input Slider] --> O(Filtered Data) + N[Input Checkboxes] --> O + O --> P{dataframe display} + + Q[Input Slider] --> S(Filtered Data) + R[Input Checkboxes] --> S + S --> T{scatter plot} + + U[Input Slider] --> W(Filtered Data) + V[Input Checkboxes] --> W + W --> X{ridgeplot} +``` + +It would be great if we calculated the filtered data **once** +and re-used it across all our output components. +```{mermaid} +flowchart LR + A[Input Slider] --> C(Filtered Data) + B[Input Checkboxes] --> C + C --> D{value box 1} + C --> E{value box 2} + C --> F{value box 3} + C --> G{dataframe display} + C --> H{scatter plot} + C --> I{ridgeplot} +``` diff --git a/tutorials/intro-express/6-reactivity.quarto_ipynb b/tutorials/intro-express/6-reactivity.quarto_ipynb new file mode 100644 index 00000000..d7fc1b83 --- /dev/null +++ b/tutorials/intro-express/6-reactivity.quarto_ipynb @@ -0,0 +1,192 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: Reactivity\n", + "---\n", + "\n", + "\n", + "We've been using the term \"reactive\" a lot during these tutorials.\n", + "But what does it mean?\n", + "It's actually more than \"user interacts with input and new value gets calculated\".\n", + "Reactivity is actually what makes Shiny special:\n", + "when an input changes, only the minimum amount of calculations are made to update the outputs.\n", + "This makes shiny very efficient.\n", + "\n", + "Shiny knows to re-execute **reactive functions** (e.g., `render` functions) when their **reactive dependencies** (e.g., `input`) change.\n", + "There are other main forms of reactive functions and dependencies:\n", + "\n", + "* Calculations with `@reactive.calc`\n", + " * Write your reactive calculation once, then call it as needed.\n", + "* Side effects with `@reactive.effect`\n", + " * Effects are similar to `@render.*` functions, but they don't return anything. They're used for their side-effects (e.g., writing to a database, sending an email, etc.)\n", + "* Reactive values with `reactive.value`\n", + " * Create `input`-like values that aren't tied to input controls and can be updated. They're often used to maintain state in an app.\n", + "\n", + "In this lesson we'll focus on the `@reactive.calc`, let's see why we may want reactive calculations.\n", + "\n", + "\n", + "```{mermaid}\n", + "flowchart LR\n", + " A[Input] --> B(Calculated from Input)\n", + " B --> C{Value calculated from the input calc}\n", + " B --> D[Another value calculated from the input calc]\n", + "```\n", + "\n", + "\n", + "Let's say we have an input (A),\n", + "this input creates a value in the application (B).\n", + "But what if another part of the application needs to use this calculated value (C)\n", + "or another part (D)?\n", + "\n", + "Similar to why we create variables in python to capture intermediate values,\n", + "we can save these intermediate \"reactive\" calculations in shiny.\n", + "\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| components: [editor, viewer]\n", + "#| layout: vertical\n", + "#| viewerHeight: 200\n", + "\n", + "from shiny import reactive\n", + "from shiny.express import input, render, ui\n", + "\n", + "ui.input_slider(\"x\", \"Slider value\", min=0, max=100, value=10)\n", + "\n", + "# we need to make a calculation from an input value\n", + "@render.text\n", + "def x_squared_text():\n", + " return f\"Squared value: {input.x() ** 2}\"\n", + "\n", + "# we can save this calculation to be used later\n", + "@reactive.calc\n", + "def x_squared():\n", + " return input.x() ** 2\n", + "\n", + "# we can use that saved calculation\n", + "@render.text\n", + "def x_squared_calc_text():\n", + " return f\"Saved squared: {x_squared()}\"\n", + "\n", + "# we can build on top of that saved calculation\n", + "@render.text\n", + "def x_squared_half_calc_text():\n", + " return f\"Build on squared value: {x_squared() / 2}\"\n", + "\n", + "# we don't need to re-calculate everything from the input again\n", + "@render.text\n", + "def x_squared_half_text():\n", + " return f\"Recalculate from input: {input.x() ** 2 / 2}\"\n", + "```\n", + "\n", + "The app above shows that we only need to make the `input.x() ** 2` calculation **once**,\n", + "so we do not need to repeat that calculation, `input.x() ** 2 / 2`.\n", + "\n", + "This idea is really similar to the following Python code,\n", + "where we save an intermediate value,\n", + "instead of repeating a calculation.\n" + ], + "id": "d17d93f0" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "initial_input_value = 3\n", + "\n", + "initial_squared = initial_input_value ** 2\n", + "\n", + "initial_squared_half = initial_squared / 2" + ], + "id": "c5c2e4fb", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we did not save the `initial_squared` intermediate value,\n", + "we would have to re-make that calculation when doing a square and half.\n" + ], + "id": "dfad6d5f" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "initial_squared_half = initial_input_value ** 2 / 2" + ], + "id": "a7eedb91", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Currently our application is re-filtering the data from the input components\n", + "for each output component displayed in the application.\n", + "\n", + "\n", + "```{mermaid}\n", + "flowchart LR\n", + " A[Input Slider] --> C(Filtered Data)\n", + " B[Input Checkboxes] --> C\n", + " C --> D{value box 1}\n", + "\n", + " E[Input Slider] --> G(Filtered Data)\n", + " F[Input Checkboxes] --> G\n", + " G --> H{value box 2}\n", + "\n", + " I[Input Slider] --> K(Filtered Data)\n", + " J[Input Checkboxes] --> K\n", + " K --> L{value box 3}\n", + "\n", + " M[Input Slider] --> O(Filtered Data)\n", + " N[Input Checkboxes] --> O\n", + " O --> P{dataframe display}\n", + "\n", + " Q[Input Slider] --> S(Filtered Data)\n", + " R[Input Checkboxes] --> S\n", + " S --> T{scatter plot}\n", + "\n", + " U[Input Slider] --> W(Filtered Data)\n", + " V[Input Checkboxes] --> W\n", + " W --> X{ridgeplot}\n", + "```\n", + "\n", + "\n", + "It would be great if we calculated the filtered data **once**\n", + "and re-used it across all our output components.\n", + "\n", + "```{mermaid}\n", + "flowchart LR\n", + " A[Input Slider] --> C(Filtered Data)\n", + " B[Input Checkboxes] --> C\n", + " C --> D{value box 1}\n", + " C --> E{value box 2}\n", + " C --> F{value box 3}\n", + " C --> G{dataframe display}\n", + " C --> H{scatter plot}\n", + " C --> I{ridgeplot}\n", + "```" + ], + "id": "27068e29" + } + ], + "metadata": { + "kernelspec": { + "name": "pfe_book", + "language": "python", + "display_name": "pfe_book", + "path": "/Users/danielchen/Library/Jupyter/kernels/pfe_book" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/tutorials/intro-express/7-publish.qmd b/tutorials/intro-express/7-publish.qmd new file mode 100644 index 00000000..719dc1a9 --- /dev/null +++ b/tutorials/intro-express/7-publish.qmd @@ -0,0 +1,77 @@ +--- +title: Publish and Share Your Application +--- + +## `app.py` + +At the start of these tutorials, +we've shown how to create an `app.py` file and how to run it +from either Positron, VS Code, or in the command line. + +If your file begins with the `app` prefix, +the shiny extension for VS Code will give you a play button to run +the current file as a shiny application. + +![](img/010-run_app.png) + +The play button executes the `shiny run` command in the terminal +for you, but you can manually run an application file +on your own. + +```bash +shiny run app.py +``` +This `app.py` file can be shared with others + +## Shiny Live + +Throughout these tutorials, +we provided you the code and working example in line with the text. +These applications were run using Shinylive. +It uses Shiny and WebAssembly to run Shiny applications +completely in the browser without having to set anything up. + +If you have all your code in an `app.py` file, +you can go to the shiny live editor and paste in your code. +The shiny live editor is at this location: + +:::{.callout-note} +There is no empty editor, the site will take you to the shinylive page and +default to one of the example applications. +You can copy and paste your `app.py` file into the editor and run it in the browser +::: + +You cannot save the application file in the browser, +instead you can click on the "Share" button on the corner of the Shinylive page, +and use this URL to share your application with others. +There is an option to use the link that shows the code and rendered application. + +:::{.callout-note} +Shinylive URLs are extremely long. That is because +all the code is embeded into the URL. +::: + +You can read more about Shinylive here: + + +## Connect Cloud + +[Connect Cloud](https://connect.posit.cloud/) is a free service that +allows you to publish your web applications. +You can actually use it to publish more than a shiny for python application! + +The code you want published needs to first exist in a +[GitHub](https://github.com/) +repository. +Once your code is in a github repository, +you can use the Posit Connect Cloud interface to link to the repository, +and the service will automatically look for your `app.py` file to publish. +You can follow these instructions from the connect cloud shiny for python +publishing page: + + +If you want to update your application, +you will make your changes, commit, and push them to the same github repository. +Connect Cloud has a +[republish](https://docs.posit.co/connect-cloud/user/manage/content_page.html#republish) +feature on the main page that will re-deploy your application. diff --git a/tutorials/intro-express/8-next.qmd b/tutorials/intro-express/8-next.qmd new file mode 100644 index 00000000..48f92100 --- /dev/null +++ b/tutorials/intro-express/8-next.qmd @@ -0,0 +1,46 @@ +--- +title: "What's Next?" +--- + +## Templates + +This dashboard is one of many example dashboards available on the +[Shiny for Python Templates Page](https://shiny.posit.co/py/templates/). + +If you want to download and run this application, +you can visit the +[Restaurant tips dashboard template page](https://shiny.posit.co/py/templates/dashboard-tips/) +and follow the command to downlaod the application + +```bash +shiny create --template dashboard-tips --mode express --github posit-dev/py-shiny-templates +``` + +This will create a `dashboard-tips` folder in your current directory. +The output of the command will then prompt you to install the dependencies for the application +using `pip`. + +```bash +$ shiny create --template dashboard-tips --mode express --github posit-dev/py-shiny-templates +ℹ Using GitHub repository posit-dev/py-shiny-templates. +… Creating Restaurant tips dashboard Shiny app... +? Enter destination directory: ./dashboard-tips +✓ Created Shiny app at dashboard-tips + +→ Next steps: +- Install required dependencies: + cd dashboard-tips + pip install -r requirements.txt +- Open and edit the app file: dashboard-tips/app.py +``` + +To run the example tips dashboard, +you can use the same `shiny run` command we did in the previous exercise. +The application code is in the `app.py` file. + +```bash +shiny run +``` + +There are a few other files and modules that are in this example application. +We will spend the next few lessons of this tutorial going though each of the components. diff --git a/tutorials/intro-express/_metadata.yml b/tutorials/intro-express/_metadata.yml new file mode 100644 index 00000000..62df4970 --- /dev/null +++ b/tutorials/intro-express/_metadata.yml @@ -0,0 +1 @@ +sidebar: tutorial-express-intro diff --git a/tutorials/intro-express/img/010-run_app-cropped_editor.png b/tutorials/intro-express/img/010-run_app-cropped_editor.png new file mode 100644 index 00000000..9ad7ef1d Binary files /dev/null and b/tutorials/intro-express/img/010-run_app-cropped_editor.png differ diff --git a/tutorials/intro-express/img/010-run_app.png b/tutorials/intro-express/img/010-run_app.png new file mode 100644 index 00000000..51adbdbc Binary files /dev/null and b/tutorials/intro-express/img/010-run_app.png differ diff --git a/tutorials/intro-express/tips.csv b/tutorials/intro-express/tips.csv new file mode 100644 index 00000000..856a65a6 --- /dev/null +++ b/tutorials/intro-express/tips.csv @@ -0,0 +1,245 @@ +total_bill,tip,sex,smoker,day,time,size +16.99,1.01,Female,No,Sun,Dinner,2 +10.34,1.66,Male,No,Sun,Dinner,3 +21.01,3.5,Male,No,Sun,Dinner,3 +23.68,3.31,Male,No,Sun,Dinner,2 +24.59,3.61,Female,No,Sun,Dinner,4 +25.29,4.71,Male,No,Sun,Dinner,4 +8.77,2.0,Male,No,Sun,Dinner,2 +26.88,3.12,Male,No,Sun,Dinner,4 +15.04,1.96,Male,No,Sun,Dinner,2 +14.78,3.23,Male,No,Sun,Dinner,2 +10.27,1.71,Male,No,Sun,Dinner,2 +35.26,5.0,Female,No,Sun,Dinner,4 +15.42,1.57,Male,No,Sun,Dinner,2 +18.43,3.0,Male,No,Sun,Dinner,4 +14.83,3.02,Female,No,Sun,Dinner,2 +21.58,3.92,Male,No,Sun,Dinner,2 +10.33,1.67,Female,No,Sun,Dinner,3 +16.29,3.71,Male,No,Sun,Dinner,3 +16.97,3.5,Female,No,Sun,Dinner,3 +20.65,3.35,Male,No,Sat,Dinner,3 +17.92,4.08,Male,No,Sat,Dinner,2 +20.29,2.75,Female,No,Sat,Dinner,2 +15.77,2.23,Female,No,Sat,Dinner,2 +39.42,7.58,Male,No,Sat,Dinner,4 +19.82,3.18,Male,No,Sat,Dinner,2 +17.81,2.34,Male,No,Sat,Dinner,4 +13.37,2.0,Male,No,Sat,Dinner,2 +12.69,2.0,Male,No,Sat,Dinner,2 +21.7,4.3,Male,No,Sat,Dinner,2 +19.65,3.0,Female,No,Sat,Dinner,2 +9.55,1.45,Male,No,Sat,Dinner,2 +18.35,2.5,Male,No,Sat,Dinner,4 +15.06,3.0,Female,No,Sat,Dinner,2 +20.69,2.45,Female,No,Sat,Dinner,4 +17.78,3.27,Male,No,Sat,Dinner,2 +24.06,3.6,Male,No,Sat,Dinner,3 +16.31,2.0,Male,No,Sat,Dinner,3 +16.93,3.07,Female,No,Sat,Dinner,3 +18.69,2.31,Male,No,Sat,Dinner,3 +31.27,5.0,Male,No,Sat,Dinner,3 +16.04,2.24,Male,No,Sat,Dinner,3 +17.46,2.54,Male,No,Sun,Dinner,2 +13.94,3.06,Male,No,Sun,Dinner,2 +9.68,1.32,Male,No,Sun,Dinner,2 +30.4,5.6,Male,No,Sun,Dinner,4 +18.29,3.0,Male,No,Sun,Dinner,2 +22.23,5.0,Male,No,Sun,Dinner,2 +32.4,6.0,Male,No,Sun,Dinner,4 +28.55,2.05,Male,No,Sun,Dinner,3 +18.04,3.0,Male,No,Sun,Dinner,2 +12.54,2.5,Male,No,Sun,Dinner,2 +10.29,2.6,Female,No,Sun,Dinner,2 +34.81,5.2,Female,No,Sun,Dinner,4 +9.94,1.56,Male,No,Sun,Dinner,2 +25.56,4.34,Male,No,Sun,Dinner,4 +19.49,3.51,Male,No,Sun,Dinner,2 +38.01,3.0,Male,Yes,Sat,Dinner,4 +26.41,1.5,Female,No,Sat,Dinner,2 +11.24,1.76,Male,Yes,Sat,Dinner,2 +48.27,6.73,Male,No,Sat,Dinner,4 +20.29,3.21,Male,Yes,Sat,Dinner,2 +13.81,2.0,Male,Yes,Sat,Dinner,2 +11.02,1.98,Male,Yes,Sat,Dinner,2 +18.29,3.76,Male,Yes,Sat,Dinner,4 +17.59,2.64,Male,No,Sat,Dinner,3 +20.08,3.15,Male,No,Sat,Dinner,3 +16.45,2.47,Female,No,Sat,Dinner,2 +3.07,1.0,Female,Yes,Sat,Dinner,1 +20.23,2.01,Male,No,Sat,Dinner,2 +15.01,2.09,Male,Yes,Sat,Dinner,2 +12.02,1.97,Male,No,Sat,Dinner,2 +17.07,3.0,Female,No,Sat,Dinner,3 +26.86,3.14,Female,Yes,Sat,Dinner,2 +25.28,5.0,Female,Yes,Sat,Dinner,2 +14.73,2.2,Female,No,Sat,Dinner,2 +10.51,1.25,Male,No,Sat,Dinner,2 +17.92,3.08,Male,Yes,Sat,Dinner,2 +27.2,4.0,Male,No,Thur,Lunch,4 +22.76,3.0,Male,No,Thur,Lunch,2 +17.29,2.71,Male,No,Thur,Lunch,2 +19.44,3.0,Male,Yes,Thur,Lunch,2 +16.66,3.4,Male,No,Thur,Lunch,2 +10.07,1.83,Female,No,Thur,Lunch,1 +32.68,5.0,Male,Yes,Thur,Lunch,2 +15.98,2.03,Male,No,Thur,Lunch,2 +34.83,5.17,Female,No,Thur,Lunch,4 +13.03,2.0,Male,No,Thur,Lunch,2 +18.28,4.0,Male,No,Thur,Lunch,2 +24.71,5.85,Male,No,Thur,Lunch,2 +21.16,3.0,Male,No,Thur,Lunch,2 +28.97,3.0,Male,Yes,Fri,Dinner,2 +22.49,3.5,Male,No,Fri,Dinner,2 +5.75,1.0,Female,Yes,Fri,Dinner,2 +16.32,4.3,Female,Yes,Fri,Dinner,2 +22.75,3.25,Female,No,Fri,Dinner,2 +40.17,4.73,Male,Yes,Fri,Dinner,4 +27.28,4.0,Male,Yes,Fri,Dinner,2 +12.03,1.5,Male,Yes,Fri,Dinner,2 +21.01,3.0,Male,Yes,Fri,Dinner,2 +12.46,1.5,Male,No,Fri,Dinner,2 +11.35,2.5,Female,Yes,Fri,Dinner,2 +15.38,3.0,Female,Yes,Fri,Dinner,2 +44.3,2.5,Female,Yes,Sat,Dinner,3 +22.42,3.48,Female,Yes,Sat,Dinner,2 +20.92,4.08,Female,No,Sat,Dinner,2 +15.36,1.64,Male,Yes,Sat,Dinner,2 +20.49,4.06,Male,Yes,Sat,Dinner,2 +25.21,4.29,Male,Yes,Sat,Dinner,2 +18.24,3.76,Male,No,Sat,Dinner,2 +14.31,4.0,Female,Yes,Sat,Dinner,2 +14.0,3.0,Male,No,Sat,Dinner,2 +7.25,1.0,Female,No,Sat,Dinner,1 +38.07,4.0,Male,No,Sun,Dinner,3 +23.95,2.55,Male,No,Sun,Dinner,2 +25.71,4.0,Female,No,Sun,Dinner,3 +17.31,3.5,Female,No,Sun,Dinner,2 +29.93,5.07,Male,No,Sun,Dinner,4 +10.65,1.5,Female,No,Thur,Lunch,2 +12.43,1.8,Female,No,Thur,Lunch,2 +24.08,2.92,Female,No,Thur,Lunch,4 +11.69,2.31,Male,No,Thur,Lunch,2 +13.42,1.68,Female,No,Thur,Lunch,2 +14.26,2.5,Male,No,Thur,Lunch,2 +15.95,2.0,Male,No,Thur,Lunch,2 +12.48,2.52,Female,No,Thur,Lunch,2 +29.8,4.2,Female,No,Thur,Lunch,6 +8.52,1.48,Male,No,Thur,Lunch,2 +14.52,2.0,Female,No,Thur,Lunch,2 +11.38,2.0,Female,No,Thur,Lunch,2 +22.82,2.18,Male,No,Thur,Lunch,3 +19.08,1.5,Male,No,Thur,Lunch,2 +20.27,2.83,Female,No,Thur,Lunch,2 +11.17,1.5,Female,No,Thur,Lunch,2 +12.26,2.0,Female,No,Thur,Lunch,2 +18.26,3.25,Female,No,Thur,Lunch,2 +8.51,1.25,Female,No,Thur,Lunch,2 +10.33,2.0,Female,No,Thur,Lunch,2 +14.15,2.0,Female,No,Thur,Lunch,2 +16.0,2.0,Male,Yes,Thur,Lunch,2 +13.16,2.75,Female,No,Thur,Lunch,2 +17.47,3.5,Female,No,Thur,Lunch,2 +34.3,6.7,Male,No,Thur,Lunch,6 +41.19,5.0,Male,No,Thur,Lunch,5 +27.05,5.0,Female,No,Thur,Lunch,6 +16.43,2.3,Female,No,Thur,Lunch,2 +8.35,1.5,Female,No,Thur,Lunch,2 +18.64,1.36,Female,No,Thur,Lunch,3 +11.87,1.63,Female,No,Thur,Lunch,2 +9.78,1.73,Male,No,Thur,Lunch,2 +7.51,2.0,Male,No,Thur,Lunch,2 +14.07,2.5,Male,No,Sun,Dinner,2 +13.13,2.0,Male,No,Sun,Dinner,2 +17.26,2.74,Male,No,Sun,Dinner,3 +24.55,2.0,Male,No,Sun,Dinner,4 +19.77,2.0,Male,No,Sun,Dinner,4 +29.85,5.14,Female,No,Sun,Dinner,5 +48.17,5.0,Male,No,Sun,Dinner,6 +25.0,3.75,Female,No,Sun,Dinner,4 +13.39,2.61,Female,No,Sun,Dinner,2 +16.49,2.0,Male,No,Sun,Dinner,4 +21.5,3.5,Male,No,Sun,Dinner,4 +12.66,2.5,Male,No,Sun,Dinner,2 +16.21,2.0,Female,No,Sun,Dinner,3 +13.81,2.0,Male,No,Sun,Dinner,2 +17.51,3.0,Female,Yes,Sun,Dinner,2 +24.52,3.48,Male,No,Sun,Dinner,3 +20.76,2.24,Male,No,Sun,Dinner,2 +31.71,4.5,Male,No,Sun,Dinner,4 +10.59,1.61,Female,Yes,Sat,Dinner,2 +10.63,2.0,Female,Yes,Sat,Dinner,2 +50.81,10.0,Male,Yes,Sat,Dinner,3 +15.81,3.16,Male,Yes,Sat,Dinner,2 +7.25,5.15,Male,Yes,Sun,Dinner,2 +31.85,3.18,Male,Yes,Sun,Dinner,2 +16.82,4.0,Male,Yes,Sun,Dinner,2 +32.9,3.11,Male,Yes,Sun,Dinner,2 +17.89,2.0,Male,Yes,Sun,Dinner,2 +14.48,2.0,Male,Yes,Sun,Dinner,2 +9.6,4.0,Female,Yes,Sun,Dinner,2 +34.63,3.55,Male,Yes,Sun,Dinner,2 +34.65,3.68,Male,Yes,Sun,Dinner,4 +23.33,5.65,Male,Yes,Sun,Dinner,2 +45.35,3.5,Male,Yes,Sun,Dinner,3 +23.17,6.5,Male,Yes,Sun,Dinner,4 +40.55,3.0,Male,Yes,Sun,Dinner,2 +20.69,5.0,Male,No,Sun,Dinner,5 +20.9,3.5,Female,Yes,Sun,Dinner,3 +30.46,2.0,Male,Yes,Sun,Dinner,5 +18.15,3.5,Female,Yes,Sun,Dinner,3 +23.1,4.0,Male,Yes,Sun,Dinner,3 +15.69,1.5,Male,Yes,Sun,Dinner,2 +19.81,4.19,Female,Yes,Thur,Lunch,2 +28.44,2.56,Male,Yes,Thur,Lunch,2 +15.48,2.02,Male,Yes,Thur,Lunch,2 +16.58,4.0,Male,Yes,Thur,Lunch,2 +7.56,1.44,Male,No,Thur,Lunch,2 +10.34,2.0,Male,Yes,Thur,Lunch,2 +43.11,5.0,Female,Yes,Thur,Lunch,4 +13.0,2.0,Female,Yes,Thur,Lunch,2 +13.51,2.0,Male,Yes,Thur,Lunch,2 +18.71,4.0,Male,Yes,Thur,Lunch,3 +12.74,2.01,Female,Yes,Thur,Lunch,2 +13.0,2.0,Female,Yes,Thur,Lunch,2 +16.4,2.5,Female,Yes,Thur,Lunch,2 +20.53,4.0,Male,Yes,Thur,Lunch,4 +16.47,3.23,Female,Yes,Thur,Lunch,3 +26.59,3.41,Male,Yes,Sat,Dinner,3 +38.73,3.0,Male,Yes,Sat,Dinner,4 +24.27,2.03,Male,Yes,Sat,Dinner,2 +12.76,2.23,Female,Yes,Sat,Dinner,2 +30.06,2.0,Male,Yes,Sat,Dinner,3 +25.89,5.16,Male,Yes,Sat,Dinner,4 +48.33,9.0,Male,No,Sat,Dinner,4 +13.27,2.5,Female,Yes,Sat,Dinner,2 +28.17,6.5,Female,Yes,Sat,Dinner,3 +12.9,1.1,Female,Yes,Sat,Dinner,2 +28.15,3.0,Male,Yes,Sat,Dinner,5 +11.59,1.5,Male,Yes,Sat,Dinner,2 +7.74,1.44,Male,Yes,Sat,Dinner,2 +30.14,3.09,Female,Yes,Sat,Dinner,4 +12.16,2.2,Male,Yes,Fri,Lunch,2 +13.42,3.48,Female,Yes,Fri,Lunch,2 +8.58,1.92,Male,Yes,Fri,Lunch,1 +15.98,3.0,Female,No,Fri,Lunch,3 +13.42,1.58,Male,Yes,Fri,Lunch,2 +16.27,2.5,Female,Yes,Fri,Lunch,2 +10.09,2.0,Female,Yes,Fri,Lunch,2 +20.45,3.0,Male,No,Sat,Dinner,4 +13.28,2.72,Male,No,Sat,Dinner,2 +22.12,2.88,Female,Yes,Sat,Dinner,2 +24.01,2.0,Male,Yes,Sat,Dinner,4 +15.69,3.0,Male,Yes,Sat,Dinner,3 +11.61,3.39,Male,No,Sat,Dinner,2 +10.77,1.47,Male,No,Sat,Dinner,2 +15.53,3.0,Male,Yes,Sat,Dinner,2 +10.07,1.25,Male,No,Sat,Dinner,2 +12.6,1.0,Male,Yes,Sat,Dinner,2 +32.83,1.17,Male,Yes,Sat,Dinner,2 +35.83,4.67,Female,No,Sat,Dinner,3 +29.03,5.92,Male,No,Sat,Dinner,3 +27.18,2.0,Female,Yes,Sat,Dinner,2 +22.67,2.0,Male,Yes,Sat,Dinner,2 +17.82,1.75,Male,No,Sat,Dinner,2 +18.78,3.0,Female,No,Thur,Dinner,2