diff --git a/_quarto.yml b/_quarto.yml index c78f875c..e3d7998e 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -73,19 +73,27 @@ website: logo-alt: The logo for Shiny for Python search: true left: - - text: "Learn Shiny" - file: docs/overview.qmd + - text: "Tutorials" + menu: + - text: "Getting Started" + href: docs/learning_streams/getting_started/01-welcome.qmd + - text: "Tutorials" + href: docs/learning_streams/tutorials.qmd + - text: "Learning Hub" + href: docs/overview.qmd + - text: "Components" menu: - - text: "Components" - file: components/index.qmd + - text: "Inputs" + href: components/index.qmd#inputs + icon: sliders + - text: "Outputs" + file: components/index.qmd#outputs icon: sliders - - text: "Layouts" + - text: "UI Layouts" file: layouts/index.qmd icon: layout-text-window-reverse - - text: "Templates" - file: templates/index.qmd - icon: code-square + - text: "Deploy" menu: - text: "Overview" @@ -93,12 +101,21 @@ website: - docs/deploy-cloud.qmd - docs/deploy-on-prem.qmd - docs/shinylive.qmd - - text: "Gallery" - file: gallery/index.qmd + + - text: "Examples" + menu: + - text: "Gallery" + href: gallery/index.qmd + - text: "Templates" + href: templates/index.qmd + + right: + - text: "Playground" href: https://shinylive.io/py/examples/ target: _blank - - text: "Reference" + + - text: "API Reference" menu: - text: "Shiny Express" href: api/express/index.qmd @@ -227,6 +244,22 @@ website: href: "/layouts/arrange/index.html#controlling-for-page-width-and-height" - id: docs + style: "floating" + collapse-level: 2 + align: left + contents: + + - section: "Learn Shiny Express" + contents: + - docs/learning_streams/getting_started/01-welcome.qmd + - docs/learning_streams/getting_started/02-ui.qmd + - docs/learning_streams/getting_started/03-inputs.qmd + - docs/learning_streams/getting_started/04-scripts.qmd + - docs/learning_streams/getting_started/05-outputs.qmd + - docs/learning_streams/getting_started/06-reactive.qmd + - docs/learning_streams/getting_started/07-publish.qmd + + - id: book style: "floating" collapse-level: 2 align: left diff --git a/docs/learning_streams/getting_started/01-welcome.qmd b/docs/learning_streams/getting_started/01-welcome.qmd new file mode 100644 index 00000000..4c4f7a16 --- /dev/null +++ b/docs/learning_streams/getting_started/01-welcome.qmd @@ -0,0 +1,310 @@ +--- +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. + +## Installing Shiny + +:::::: {.panel-tabset} + +## Windows + +::: {.panel-tabset} + +## pip + +```bash +python -m venv shiny-env # create virtual environment +shiny-env\Scripts\activate # activate environment +pip install -U shiny # install shiny +``` + +## conda + +```bash +# create virtual environment and install shiny +conda create -n shiny-env -c conda-forge shiny + +# activate environment +conda activate shiny-env +``` + +## mamba + +```bash +# create virtual environment and install shiny +mamba create -n shiny-env -c conda-forge shiny + +# activate environment +mamba activate shiny-env +``` + +::: + +## MacOS + +::: {.panel-tabset} + +## pip + +```bash +python -m venv shiny-env # create virtual environment +source shiny-env/bin/activate # activate environment +pip install -U shiny # install shiny +``` + +## conda + +```bash +# create virtual environment and install shiny +conda create -n shiny-env -c conda-forge shiny + +# activate environment +conda activate shiny-env +``` + +## mamba + +```bash +# create virtual environment and install shiny +mamba create -n shiny-env -c conda-forge shiny + +# activate environment +mamba activate shiny-env +``` +::: + +## Linux + +::: {.panel-tabset} + +## pip + +```bash +python -m venv shiny-env # create virtual environment +source shiny-env/bin/activate # activate environment +pip install -U shiny # install shiny +``` + +## conda + +```bash +# create virtual environment and install shiny +conda create -n shiny-env -c conda-forge shiny + +# activate environment +conda activate shiny-env +``` + +## mamba + +```bash +# create virtual environment and install shiny +mamba create -n shiny-env -c conda-forge shiny + +# activate environment +mamba activate shiny-env +``` +::: + +:::::: + + +We will be using [Positron](https://positron.posit.co/) in our tutorials, +but you can also use [Visual Studio Code](https://code.visualstudio.com/). +Whether you are using Positron, or VS Code, +you will need to make sure you have the +[VS Code Shiny Extension](https://marketplace.visualstudio.com/items?itemName=Posit.shiny) +installed. + +If you are working with VSCode and Positron, +make sure your current python environment has +the `ipykernel` package installed. +We assume you are already in the environment we set up in the previous installing shiny section. + + +::: {.panel-tabset} + +## pip + +```bash +pip install ipykernel +``` + +## conda + +```bash +conda install -c conda-forge ipykernel +``` + +## mamba + +```bash +mamba install -c conda-forge ipykernel +``` + +::: + +## Parts of a Shiny Application + +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. + +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. + +```{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()}" +``` + +This example 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. + +* Inputs are created via `ui.input_*()` functions. + * The first argument is the input's `id`, which is used to read the input's value. +* Outputs are created by decorating a function with `@render.*`. + * Inside a `render` function, `input` values can be read [reactively](#reactivity). + * When those `input` values change, Shiny knows how to minimally re-render output. +* Layouts are 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! + +![](img/010-run_app.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-getting_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`) +::: + +::: + +## 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 +``` + +::: {.callout-note} +If you named your application `app.py` you can omit it form the command and only use `shiny run`. +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 the VS Code shiny extension +to display the run app button. +::: + +:::{.callout-tip} +## Helpful run options + +Some useful options you can pass the `shiny run` command are: + +- `--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. +- `--reload`: Enables auto-reload + +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} + + + +::: + +::::: diff --git a/docs/learning_streams/getting_started/02-ui.qmd b/docs/learning_streams/getting_started/02-ui.qmd new file mode 100644 index 00000000..fb511f1a --- /dev/null +++ b/docs/learning_streams/getting_started/02-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. + +![](../../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/docs/learning_streams/getting_started/03-inputs.qmd b/docs/learning_streams/getting_started/03-inputs.qmd new file mode 100644 index 00000000..838f9b1f --- /dev/null +++ b/docs/learning_streams/getting_started/03-inputs.qmd @@ -0,0 +1,199 @@ +--- +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. + +![](../../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 (empty for now) +with ui.sidebar(open="desktop"): + ui.input_slider("slider", "Bill amount", min=0, max=100, value=[0, 100]) + ui.input_checkbox_group( + "checkbox_group", + "Food service", + { + "lunch": "Lunch", + "dinner": "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/docs/learning_streams/getting_started/04-scripts.qmd b/docs/learning_streams/getting_started/04-scripts.qmd new file mode 100644 index 00000000..7433294d --- /dev/null +++ b/docs/learning_streams/getting_started/04-scripts.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. + +![](../../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("docs/learning_streams/getting_started/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/docs/learning_streams/getting_started/05-outputs.qmd b/docs/learning_streams/getting_started/05-outputs.qmd new file mode 100644 index 00000000..817c5845 --- /dev/null +++ b/docs/learning_streams/getting_started/05-outputs.qmd @@ -0,0 +1,514 @@ +--- +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 (empty for now) +with ui.sidebar(open="desktop"): + ui.input_slider("slider", "Bill amount", min=0, max=100, value=[0, 100]) + ui.input_checkbox_group( + "checkbox_group", + "Food service", + { + "lunch": "Lunch", + "dinner": "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 (empty for now) +with ui.sidebar(open="desktop"): + ui.input_slider("slider", "Bill amount", min=0, max=100, value=[0, 100]) + ui.input_checkbox_group( + "checkbox_group", + "Food service", + { + "lunch": "Lunch", + "dinner": "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: 500 + +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 + +# you may need to change the path to the tips.csv file +tips = pd.read_csv("docs/learning_streams/getting_started/tips.csv") + +# title +ui.page_opts(title="Restaurant tipping", fillable=True) + +# sidebar (empty for now) +with ui.sidebar(open="desktop"): + ui.input_slider( + "slider", + "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( + "checkbox_group", + "Food service", + { + "lunch": "Lunch", + "dinner": "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 +``` +::: diff --git a/docs/learning_streams/getting_started/06-reactive.qmd b/docs/learning_streams/getting_started/06-reactive.qmd new file mode 100644 index 00000000..0d8654de --- /dev/null +++ b/docs/learning_streams/getting_started/06-reactive.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/docs/learning_streams/getting_started/07-publish.qmd b/docs/learning_streams/getting_started/07-publish.qmd new file mode 100644 index 00000000..719dc1a9 --- /dev/null +++ b/docs/learning_streams/getting_started/07-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/docs/learning_streams/getting_started/08-next.qmd b/docs/learning_streams/getting_started/08-next.qmd new file mode 100644 index 00000000..8c10a08b --- /dev/null +++ b/docs/learning_streams/getting_started/08-next.qmd @@ -0,0 +1,43 @@ + +## 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/docs/learning_streams/getting_started/img/010-run_app.png b/docs/learning_streams/getting_started/img/010-run_app.png new file mode 100644 index 00000000..51adbdbc Binary files /dev/null and b/docs/learning_streams/getting_started/img/010-run_app.png differ diff --git a/docs/learning_streams/getting_started/tips.csv b/docs/learning_streams/getting_started/tips.csv new file mode 100644 index 00000000..856a65a6 --- /dev/null +++ b/docs/learning_streams/getting_started/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 diff --git a/docs/learning_streams/tutorials.qmd b/docs/learning_streams/tutorials.qmd new file mode 100644 index 00000000..21081f07 --- /dev/null +++ b/docs/learning_streams/tutorials.qmd @@ -0,0 +1,124 @@ +--- +css: tutorial.css +--- + +```{=html} + +``` + +## Express-Only Tutorials + +Shiny Express is the easiest and quickest way to start with Shiny. +Create applications at the speed of thought. + +```{=html} + +``` + +## Express and Core Tutorials + +Here are the more general tutorials to get you started with all things Shiny. + +```{=html} + +``` + +## 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} + +```