diff --git a/docs/apps/000-penguins/app.py b/docs/apps/000-penguins/app.py new file mode 100644 index 0000000..8fb5317 --- /dev/null +++ b/docs/apps/000-penguins/app.py @@ -0,0 +1,57 @@ +from pathlib import Path + +import pandas as pd +import seaborn as sns + +from shiny.express import input, render, ui +from shiny import reactive, req + +sns.set_theme() + +df = pd.read_csv(Path(__file__).parent / "penguins.csv", na_values="NA") +numeric_cols = df.select_dtypes(include=["float64"]).columns.tolist() +species = df["Species"].unique().tolist() +species.sort() + + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_selectize( + "xvar", "X variable", numeric_cols, selected="Bill Length (mm)" + ) + ui.input_selectize( + "yvar", "Y variable", numeric_cols, selected="Bill Depth (mm)" + ) + ui.input_checkbox_group( + "species", "Filter by species", species, selected=species + ) + ui.hr() + ui.input_switch("by_species", "Show species", value=True) + ui.input_switch("show_margins", "Show marginal plots", value=True) + + @render.plot + def scatter(): + """Generates a plot for Shiny to display to the user""" + + # The plotting function to use depends on whether margins are desired + plotfunc = sns.jointplot if input.show_margins() else sns.scatterplot + + plotfunc( + data=filtered_df(), + x=input.xvar(), + y=input.yvar(), + hue="Species" if input.by_species() else None, + hue_order=species, + legend=False, + ) + +@reactive.calc +def filtered_df() -> pd.DataFrame: + """Returns a Pandas data frame that includes only the desired rows""" + + # This calculation "requires" that at least one species is selected + req(len(input.species()) > 0) + + # Filter the rows so we only include the desired species + return df[df["Species"].isin(input.species())] + diff --git a/apps/examples/0.0-penguins/app.py b/docs/apps/000-penguins/app_core.py similarity index 98% rename from apps/examples/0.0-penguins/app.py rename to docs/apps/000-penguins/app_core.py index 2d0459b..e08d9ae 100644 --- a/apps/examples/0.0-penguins/app.py +++ b/docs/apps/000-penguins/app_core.py @@ -3,7 +3,6 @@ import pandas as pd import seaborn as sns -import shiny.experimental as x from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui sns.set_theme() diff --git a/apps/examples/0.0-penguins/penguins.csv b/docs/apps/000-penguins/penguins.csv similarity index 100% rename from apps/examples/0.0-penguins/penguins.csv rename to docs/apps/000-penguins/penguins.csv diff --git a/apps/examples/0.0-penguins/requirements.txt b/docs/apps/000-penguins/requirements.txt similarity index 100% rename from apps/examples/0.0-penguins/requirements.txt rename to docs/apps/000-penguins/requirements.txt diff --git a/docs/shiny-express-slides.qmd b/docs/shiny-express-slides.qmd index 9073723..9584132 100644 --- a/docs/shiny-express-slides.qmd +++ b/docs/shiny-express-slides.qmd @@ -75,7 +75,7 @@ We need to tell the compute what to do when the user clicks. # | output: asis include_shiny_folder( - "../apps/examples/0.0-penguins", components="viewer", viewer_height=700 + "apps/000-penguins", components="viewer", viewer_height=700 ) ``` diff --git a/docs/shiny-express-slides.quarto_ipynb b/docs/shiny-express-slides.quarto_ipynb new file mode 100644 index 0000000..fe649ba --- /dev/null +++ b/docs/shiny-express-slides.quarto_ipynb @@ -0,0 +1,431 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "title: \"Getting started with Shiny express\"\n", + "title-slide-attributes: \n", + " data-background-image: images/shiny-for-python-newcastle.jpg\n", + " data-background-position: bottom left\n", + " data-background-size: cover\n", + "format:\n", + " positconfslides-revealjs:\n", + " incremental: false\n", + " chalkboard: true\n", + " slide-number: c/t\n", + " code-copy: true\n", + " center-title-slide: false\n", + " code-link: true\n", + " highlight-style: a11y\n", + " width: \"1600\"\n", + " height: \"900\"\n", + " css: \"styles.css\"\n", + " filters:\n", + " - positconfslides\n", + " - reveal-auto-agenda\n", + " auto-agenda:\n", + " heading: Agenda\n", + "---" + ], + "id": "5edf717e" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | echo: false\n", + "import os\n", + "import sys\n", + "exercises_path = \"./exercises\"\n", + "if exercises_path not in sys.path:\n", + " sys.path.append(exercises_path)\n", + "\n", + "from helpers import include_shiny_folder" + ], + "id": "4646c70c", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting started\n", + "\n", + "\n", + "## Goals of Session 1\n", + "\n", + "1. Understand the mechanics of a Shiny app (what to do, not why you do it)\n", + "\n", + "2. Develop comfort with failure and debugging\n", + "\n", + "3. You know where to go for help\n", + "\n", + "## Why Shiny?\n", + "\n", + "Introduce the end state / whole game / bigger picture: web page with a server. \n", + "\n", + "The server watches the web page and responds to user events on that page.\n", + "\n", + "We need to tell the compute what to do when the user clicks.\n", + "\n", + "\n", + "## How this session works\n", + "\n", + "- This session is going to be exercise based\n", + "- All of the exercises and slides are running live on the [website](https://posit-conf-2024.github.io/intro-to-shiny-for-python/)\n", + "- You can do them on the website or open the apps in the `apps/core` folder\n", + "- If you need help, put a red sticker on your laptop; when you're done, put up a green sticker\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-101-hello-world' >}}\n", + "\n", + "\n", + "\n", + "# Hello Shiny\n", + "\n", + "## What is a Shiny app?\n" + ], + "id": "81f5c547" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# | echo: false\n", + "# | output: asis\n", + "\n", + "include_shiny_folder(\n", + " \"apps/000-penguins\", components=\"viewer\", viewer_height=700\n", + ")" + ], + "id": "0b96296f", + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key features of Shiny\n", + "\n", + "- Easy to develop\n", + "- Extensible\n", + "- Efficient\n", + "\n", + "## Why Shiny for Python?\n", + "\n", + "Shiny is great because it lets you quickly create web apps without worrying about maintaining cache, managing state, and callbacks — or even HTML, CSS, and JavaScript.\n", + "\n", + "- Shiny is designed to take you from prototype to production\n", + "- Easy enough to use for prototypes\n", + "- Efficient enough to handle complexity\n", + "- Everything you need to build production quality apps\n", + "- You don't need to throw away your Shiny apps\n", + "\n", + "# Anatomy of a Shiny App\n", + "\n", + "## You need three things\n", + "\n", + "To make a Shiny app work you need to do three things:\n", + "\n", + "1. Create a **UI section** that describes inputs and outputs\n", + "\n", + " - Can contain text, plots, buttons, sliders, etc.\n", + "\n", + "2. Create a **server section**\n", + "\n", + " - Contains your application logic\n", + " - In particular, it contains **rendering functions** that turn inputs into outputs\n", + "\n", + "3. Link the server section and UI section\n", + "\n", + " - by referring to the inputs in the server section\n", + "\n", + "\n", + "## Inputs using shiny express\n", + "\n", + "```{.python filename=\"app.py\" code-line-numbers=\"3-4\"}\n", + "from shiny.express import input, render, ui\n", + "\n", + "# UI section\n", + "ui.input_slider(id = \"n\", label = \"N\", min = 0, max = 100, value = 20)\n", + "\n", + "## Server section\n", + "@render.text\n", + "def txt():\n", + " return f\"2 * n is {input.n() * 2}\"\n", + "```\n", + "\n", + "
\n", + "\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| viewerHeight: 150\n", + "\n", + "from shiny.express import input, render, ui\n", + "\n", + "# UI section\n", + "ui.input_slider(\"n\", \"N\", 0, 100, 20)\n", + "\n", + "## Server section\n", + "@render.text\n", + "def txt():\n", + " return f\"2 * n is {input.n() * 2}\"\n", + "```\n", + "\n", + "## Server section using shiny express\n", + "\n", + "``` {.python filename=\"app.py\" code-line-numbers=\"6-9\"}\n", + "from shiny.express import input, render, ui\n", + "\n", + "# UI section\n", + "ui.input_slider(id = \"n\", label = \"N\", min = 0, max = 100, value = 20)\n", + "\n", + "## Server section\n", + "@render.text\n", + "def txt():\n", + " return f\"2 * n is {input.n() * 2}\"\n", + "```\n", + "\n", + "
\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| viewerHeight: 150\n", + "\n", + "from shiny.express import input, render, ui\n", + "\n", + "# UI section\n", + "ui.input_slider(\"n\", \"N\", 0, 100, 20)\n", + "\n", + "## Server section\n", + "@render.text\n", + "def txt():\n", + " return f\"2 * n is {input.n() * 2}\"\n", + "```\n", + "\n", + "## Refer to inputs\n", + "\n", + "Your render function can refer to the input by calling it like a function.\n", + "\n", + "In this case, the `input_slider` has the name `\"n\"`, so you refer to it as `input.n()`.\n", + "\n", + "\n", + "```{.python filename=\"app.py\" code-line-numbers=\"9\"}\n", + "from shiny.express import input, render, ui\n", + "\n", + "# UI section\n", + "ui.input_slider(id = \"n\", label = \"N\", min = 0, max = 100, value = 20)\n", + "\n", + "## Server section\n", + "@render.text\n", + "def txt():\n", + " return f\"2 * n is {input.n() * 2}\"\n", + "```\n", + "\n", + "
\n", + "\n", + "```{shinylive-python}\n", + "#| standalone: true\n", + "#| viewerHeight: 150\n", + "\n", + "from shiny.express import input, render, ui\n", + "\n", + "# UI section\n", + "ui.input_slider(\"n\", \"N\", 0, 100, 20)\n", + "\n", + "## Server section\n", + "@render.text\n", + "def txt():\n", + " return f\"2 * n is {input.n() * 2}\"\n", + "```\n", + "\n", + "## UI functions\n", + "\n", + "- The `ui` submodule contains functions to create UI elements\n", + "- Inputs start with `ui.input_*()` and take an `id` and options\n", + "- Other functions like `ui.h1()` or `ui.p()` add static HTML to the app\n", + "- We'll get to layout functions in Session 3\n", + "\n", + "\n", + "\n", + "## Inputs component gallery\n", + "\n", + "![](images/input-component-gallery.png)\n", + "\n", + "\n", + "\n", + "\n", + "## Render functions for outputs\n", + "\n", + "The render functions define the outputs of the app.\n", + "\n", + "- Outputs start with `render_*()` and usually just take an id\n", + "\n", + "\n", + "\n", + "\n", + "## Outputs component gallery\n", + "\n", + "![](images/output-component-gallery.png)\n", + "\n", + "\n", + "\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-102-data-frame' >}}\n", + "\n", + "\n", + "\n", + "# Rendering functions on the server side\n", + "\n", + "## Server function\n", + "\n", + "- Every Shiny app needs a `server` function\n", + "- The server function executes for each user session\n", + "- This function contains **rendering functions** which define how to turn inputs into outputs\n", + "\n", + "## Rendering functions\n", + "\n", + "``` python\n", + "def server(input, output, session):\n", + " @output\n", + " @render.text\n", + " def txt():\n", + " return f\"n*2 is {input.n() * 2}\"\n", + "```\n", + "\n", + "- Functions are defined inside the main `server` function\n", + "- They need to be **decorated** with `@output` and `@render.*` in that order\n", + "- The function **name** should match the output id\n", + "- The return value is sent to the Shiny UI\n", + "\n", + "## Decorators are just functions\n", + "\n", + "``` python\n", + " @output\n", + " @render.plot(alt=\"A body mass plot of penguins\")\n", + " def mass_plot():\n", + " df = sample_data(penguins.copy(), input.sample())\n", + " df = df.loc[df[\"body_mass\"] < input.mass()]\n", + " return dist_plot(df)\n", + "```\n", + "\n", + "- Decorators are functions which take other functions\n", + "- Shiny uses them to identify reactive functions\n", + "- They can take arguments\n", + "\n", + "## Simple app example\n", + "\n", + "``` {.python code-line-numbers=\"5,10-13\"}\n", + "from shiny import Inputs, Outputs, Session, App, render, ui\n", + "\n", + "app_ui = ui.page_fluid(\n", + " ui.input_slider(\"n\", \"N\", 0, 100, 20),\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\"n*2 is {input.n() * 2}\"\n", + "\n", + "app = App(app_ui, server)\n", + "```\n", + "\n", + "## Common problems\n", + "\n", + "- An output doesn't render\n", + "- An output doesn't update when an input changes\n", + "- There's some weird error\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-103-debug' >}}\n", + "\n", + "\n", + "\n", + "# Making the link\n", + "\n", + "## Connecting inputs to outputs\n", + "\n", + "``` {.python code-line-numbers=\"5\"}\n", + "def server(input: Inputs, output: Outputs, session: Session):\n", + " @output\n", + " @render.text\n", + " def txt():\n", + " return f\"n*2 is {input.n() * 2}\"\n", + "```\n", + "\n", + "- Inputs are read by calling them like a function: `input.n()`, not `input.n`\n", + "- Referring to an input creates a reactive link between that input and the rendering function\n", + "- When the input changes, the rendering function will re-execute\n", + "- You can use multiple inputs in the same rendering function\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-104-filter' >}}\n", + "\n", + "\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-105-connect-filter' >}}\n", + "\n", + "\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-106-debug' >}}\n", + "\n", + "\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-107-debug' >}}\n", + "\n", + "\n", + "\n", + "# Recap\n", + "\n", + "## Summary\n", + "\n", + "Most Shiny app development consists of variations of these three things:\n", + "\n", + "1. Add UI elements\n", + "2. Add rendering functions\n", + "3. Connect inputs and outputs\n", + "\n", + "## Your turn\n", + "\n", + "\n", + "{{< yourturn 'express-108-plot' >}}" + ], + "id": "cea64679" + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)", + "path": "/home/andrie/github/posit-conf-2024/intro-to-shiny-for-python/.venv/share/jupyter/kernels/python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file