├── .github └── workflows │ ├── issue_workflow.yml │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __init__.py ├── _extensions ├── kmasiello │ └── positconfslides │ │ ├── _extension.yml │ │ ├── assets │ │ ├── backgrounds │ │ │ ├── 30-70-dark.svg │ │ │ ├── 30-70-light.svg │ │ │ ├── 30-70-white.svg │ │ │ ├── brackets-dark.svg │ │ │ ├── brackets-light.svg │ │ │ ├── content-dark.svg │ │ │ ├── content-light.svg │ │ │ ├── content2-light.svg │ │ │ ├── toc-dark.svg │ │ │ ├── toc-light.svg │ │ │ ├── toc-people-dark.svg │ │ │ ├── toc-people-light.svg │ │ │ ├── toc2-dark.svg │ │ │ └── toc2-light.svg │ │ └── posit-slides.js │ │ ├── custom.scss │ │ └── positconfslides.lua ├── quarto-ext │ └── shinylive │ │ ├── README.md │ │ ├── _extension.yml │ │ ├── resources │ │ └── css │ │ │ └── shinylive-quarto.css │ │ └── shinylive.lua └── shafayetShafee │ └── bsicons │ ├── _extension.yml │ ├── assets │ ├── css │ │ └── all.css │ └── webfonts │ │ ├── bootstrap-icons.woff │ │ └── bootstrap-icons.woff2 │ └── bsicons.lua ├── _publish.yml ├── _quarto.yml ├── apps ├── examples │ ├── 0.0-penguins │ │ ├── app.py │ │ ├── penguins.csv │ │ └── requirements.txt │ ├── 1.0-basic-app │ │ └── app.py │ ├── 1.1-table-only │ │ ├── app.py │ │ └── penguins.csv │ ├── 1.2-table-summary │ │ ├── app.py │ │ └── penguins.csv │ ├── 1.3-table-input-slider │ │ ├── app.py │ │ └── penguins.csv │ ├── 1.4-table-graph-input-slider │ │ ├── app.py │ │ ├── penguins.csv │ │ └── plots.py │ ├── 2.0-simple-reactive-calc │ │ └── app.py │ ├── 2.1-reactive-calc │ │ ├── app.py │ │ ├── penguins.csv │ │ └── plots.py │ ├── 3.0-modal │ │ └── app.py │ ├── 3.3-css │ │ ├── app.py │ │ └── www │ │ │ └── my_styles.css │ ├── 4.0.0-navsets │ │ └── app.py │ ├── 4.0.0.0-cards │ │ └── app.py │ ├── 4.0.1-rows-columns │ │ └── app.py │ ├── 4.0.2-nested-columns │ │ └── app.py │ ├── 4.1.1-dynamic-ui │ │ ├── app.py │ │ └── weather.csv │ └── 4.1.2-conditional-panel │ │ └── app.py ├── problem-sets │ ├── 1-getting-started │ │ ├── 1.0-hello-world │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ └── requirements.txt │ │ ├── 1.1-data-frame │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 1.2-debug │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── model_data.csv │ │ │ └── requirements.txt │ │ ├── 1.3-filter-input │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 1.4-filter-connect │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 1.5-debug │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 1.6-debug │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 1.7-add-plot │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ └── requirements.txt │ ├── 2-basic-ui │ │ ├── 2.1-sidebar │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 2.2-cards │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 2.3-cards-switch │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 2.4-layout-columns │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ └── requirements.txt │ ├── 3-reactivity │ │ ├── 3.1-reactive-calc │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 3.2-stacking-reactives │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ └── requirements.txt │ ├── 4-dynamic-ui │ │ ├── 4.1-render-express │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ └── requirements.txt │ ├── 4-ui-customization │ │ ├── 4.1-tabs │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ ├── 4.2-cards │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ ├── 4.3-layout │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ ├── 4.4-ui-composition │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ ├── 4.5-value-boxes │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ ├── 4.6-dynamic-ui │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ ├── 4.7-conditional-panel │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── weather.csv │ │ └── requirements.txt │ ├── 5-reactive-effects │ │ ├── 5.1-reactive-event │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ ├── 5.2-reactive-effect │ │ │ ├── README │ │ │ ├── app-solution.py │ │ │ ├── app.py │ │ │ ├── data_import.py │ │ │ ├── plots.py │ │ │ ├── requirements.txt │ │ │ └── simulated-data.csv │ │ └── requirements.txt │ └── requirements.txt ├── target-app │ ├── app.py │ ├── data_import.py │ ├── manifest.json │ ├── plots.py │ ├── requirements.txt │ ├── rsconnect-python │ │ └── target-app.json │ └── simulated-data.csv └── utilities │ └── multiple-choice │ ├── app.py │ └── questions.json ├── exercises ├── 1-hello-world.qmd ├── 2-basic-ui.qmd ├── 3-reactivity.qmd ├── 4-dynamic-ui.qmd └── 5-reactive-effect.qmd ├── helpers.py ├── images ├── git-download-button.png └── shiny-course_image │ ├── shiny-2x.png │ ├── shiny-2x.webp │ └── shiny.pxd │ ├── QuickLook │ ├── Icon.webp │ └── Thumbnail.webp │ ├── data │ ├── 3F96BFC4-F9B9-4D8F-A3A9-DEE75FBCD65C │ └── AF550587-953F-4997-9677-0C075842D366-76320-00013F899D49BA33 │ └── metadata.info ├── import-helpers.quarto_ipynb ├── index.qmd ├── requirements.txt └── styles.css /.github/workflows/issue_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Issue Management 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | triage_or_add_to_project: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check if the issue creator is a maintainer 13 | id: check_maintainer 14 | run: | 15 | echo "::set-output name=is_maintainer::$(curl -s -H 'Authorization: token ${{ secrets.GITHUB_TOKEN }}' \ 16 | https://api.github.com/repos/${{ github.repository }}/collaborators/${{ github.event.issue.user.login }}/permission \ 17 | | jq -r '.permission' | grep -q 'admin' && echo 'true' || echo 'false')" 18 | 19 | - name: Apply "needs triage" label to issues created by non-maintainers 20 | if: steps.check_maintainer.outputs.is_maintainer == 'false' 21 | uses: actions/github-script@v6 22 | with: 23 | script: | 24 | github.rest.issues.addLabels({ 25 | issue_number: context.issue.number, 26 | owner: context.repo.owner, 27 | repo: context.repo.repo, 28 | labels: ["needs-triage"] 29 | }) -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | push: 4 | branches: main 5 | 6 | name: Quarto Publish 7 | 8 | jobs: 9 | build-deploy: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: Check out repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Quarto 18 | uses: quarto-dev/quarto-actions/setup@v2 19 | 20 | - name: Install Python and Dependencies 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.10' 24 | cache: 'pip' 25 | - run: pip install jupyter 26 | - run: pip install -r requirements.txt 27 | 28 | - name: Render and Publish 29 | uses: quarto-dev/quarto-actions/publish@v2 30 | with: 31 | target: gh-pages 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .Rproj.user 162 | 163 | /.quarto/ 164 | .DS_Store 165 | _site 166 | site_libs 167 | 168 | /.luarc.json 169 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RStudio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive Web Dashboards with Shiny Course 2 | 3 | This is the repository for Talk Python's [Reactive Web Dashboards with Shiny course](https://training.talkpython.fm/courses/reactive-web-dashboards-with-shiny-for-data-science). This 2-hour video course is 100% free so [visit the course page and sign up](https://training.talkpython.fm/courses/reactive-web-dashboards-with-shiny-for-data-science) if you're interested. 4 | 5 | # Installation 6 | 7 | You will need to install a few things to render the website locally: 8 | 9 | 1) [Install quarto](https://quarto.org/docs/get-started/) 10 | 2) Install the shinylive python package `pip install shinylive --upgrade` 11 | 3) Install the shinylive quarto materials `quarto add quarto-ext/shinylive` 12 | 13 | ## How to edit the materials 14 | 15 | This is a quarto website, so to make changes to the course text modify the `.qmd` files, or the `_quarto.yml`. 16 | To do a live preview run `quarto preview --render html`, note that while `--render html` is a bit slower, it's the best way to see changes with the included applications. 17 | 18 | ## Creating an including Shiny Apps 19 | 20 | All of the apps live in the `apps` folder, which means that you can use VS Code to edit and test them out. 21 | To include an application insert an asis quarto chuck which looks like this: 22 | 23 | ```{python} 24 | #| echo: false 25 | #| output: asis 26 | 27 | include_shiny_folder("apps/basic-app") 28 | ``` 29 | 30 | You can also pass optins to this function to modify the behaviour of the included app. 31 | 32 | To include a set of problem tabs, your app should have two application files. `app.py` which shows the starting point for the problem and `app-solution.py` which shows the target application. 33 | You can then use the `problem_tabs` function to include the tabs. 34 | 35 | ```{python} 36 | #| echo: false 37 | #| output: asis 38 | 39 | problem_tabs("apps/basic-app") 40 | ``` 41 | 42 | ## Inserting multiple choice questions 43 | 44 | You can insert a shinylive app which displays sets of multiple choice questions by supplying a dictionary. 45 | It is a good idea to always wrap this dictionary with the `Quiz` class which validates that it is the right format for the application. 46 | 47 | ```{python} 48 | # | echo: false 49 | # | output: asis 50 | 51 | from helpers import multiple_choice_app, Quiz 52 | 53 | questions = Quiz( 54 | { 55 | "What ui input is used for plots?": { 56 | "choices": ["ui.input_plot", "ui.plot_input", "ui.plotInput"], 57 | "answer": "ui.Input_plot", 58 | }, 59 | "How do you remove a reacitve link??": { 60 | "choices": ["reactive.isolate", "req", "reactive.Effect"], 61 | "answer": "reactive.isolate", 62 | }, 63 | "What should you use to save an image of a plot to disk?": { 64 | "choices": ["reactive.Calc", "@ui.output_plot", "reactive.Effect"], 65 | "answer": "reactive.Effect", 66 | }, 67 | } 68 | ) 69 | 70 | multiple_choice_app(questions) 71 | ``` -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/__init__.py -------------------------------------------------------------------------------- /_extensions/kmasiello/positconfslides/_extension.yml: -------------------------------------------------------------------------------- 1 | title: positconfslides 2 | author: Thomas Mock, Carlos Scheidegger, Katie Masiello 3 | version: 1.0.0 4 | quarto-required: ">=1.2.0" 5 | contributes: 6 | formats: 7 | revealjs: 8 | theme: [custom.scss] 9 | filters: 10 | - positconfslides.lua 11 | 12 | -------------------------------------------------------------------------------- /_extensions/kmasiello/positconfslides/assets/backgrounds/30-70-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_extensions/kmasiello/positconfslides/assets/backgrounds/30-70-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_extensions/kmasiello/positconfslides/assets/posit-slides.js: -------------------------------------------------------------------------------- 1 | // the world's your oyster -------------------------------------------------------------------------------- /_extensions/kmasiello/positconfslides/custom.scss: -------------------------------------------------------------------------------- 1 | /*-- scss:defaults --*/ 2 | 3 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap'); 4 | @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 5 | @import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@300;400'); 6 | 7 | $font-family-sans-serif: 'Open Sans', sans-serif !default; 8 | $font-family-monospace: 'Source Code Pro', monospace !default; 9 | $presentation-heading-font: "Open Sans", sans-serif !default; 10 | $presentation-title-slide-text-align: left; 11 | $presentation-heading-font-weight: 300; 12 | $code-block-font-size: 0.80em; 13 | 14 | // colors 15 | $body-bg: #FFFFFF; 16 | $body-color: #404041; 17 | $code-color: #9a4665; 18 | $selection-bg: #93bdbf; 19 | $link-color: #3D6E74; 20 | 21 | // dark colors 22 | $dark-bg-text-color: #F0F0F0; 23 | $dark-bg-code-color: #cca2b2; 24 | $dark-bg-link-color: #93bdbf; 25 | 26 | /*-- scss:rules --*/ 27 | 28 | #title-slide .subtitle { 29 | font-size: 60px; 30 | font-weight: 600; 31 | } 32 | 33 | #title-slide .quarto-title-author-name { 34 | font-size: 50px; 35 | font-weight: 300; 36 | } 37 | 38 | #title-slide .institute { 39 | font-weight: 1000; 40 | } 41 | 42 | // dark slides 43 | section div.has-dark-background { 44 | color: $dark-bg-text-color; 45 | } 46 | 47 | section div.has-dark-background h2 { 48 | font-weight: 300 !important; 49 | color: $dark-bg-text-color; 50 | } 51 | 52 | section div.has-dark-background code { 53 | color: $dark-bg-code-color; 54 | } 55 | 56 | section div.has-dark-background a { 57 | color: $dark-bg-link-color; 58 | } 59 | 60 | section div.has-dark-background blockquote { 61 | color: #c9c9c9; 62 | } 63 | 64 | // don't add margin to last column 65 | 66 | .reveal .columns>.column:last-child>:not(ul, ol) { 67 | margin-left: 0; 68 | } 69 | 70 | // footer details 71 | .reveal .slide-number a { 72 | font-family: 'Barlow Condensed', sans-serif; 73 | } 74 | 75 | .reveal .footer p { 76 | font-family: 'Barlow Condensed', sans-serif; 77 | } 78 | 79 | // font sizes 80 | 81 | .smaller { 82 | font-size: smaller 83 | } 84 | 85 | .medium { 86 | font-size: medium 87 | } 88 | 89 | .large { 90 | font-size: 150%; 91 | } 92 | 93 | .larger { 94 | font-size: 250%; 95 | } 96 | 97 | .largest { 98 | font-size: 500%; 99 | } -------------------------------------------------------------------------------- /_extensions/kmasiello/positconfslides/positconfslides.lua: -------------------------------------------------------------------------------- 1 | function many_columns(conf) 2 | local background_image = conf.image 3 | 4 | local outerColumns = pandoc.Div({}) 5 | outerColumns.classes:insert("columns") 6 | if conf.dark then 7 | outerColumns.classes:insert("has-dark-background") 8 | end 9 | 10 | for i, v in ipairs(conf.columns) do 11 | local innerColumn = pandoc.Div(v) 12 | innerColumn.classes:insert("column") 13 | innerColumn.attributes.width = conf.widths[i]; 14 | outerColumns.content:insert(innerColumn) 15 | end 16 | 17 | local header_block = pandoc.Header(2, "") 18 | 19 | header_block.attr.attributes["background-image"] = "_extensions/positconfslides/assets/backgrounds/" .. background_image .. ".svg" 20 | header_block.attr.attributes["background-size"] = "contain" 21 | 22 | local new_blocks = pandoc.List({}) 23 | new_blocks:insert(header_block) 24 | new_blocks:insert(outerColumns) 25 | 26 | return new_blocks 27 | end 28 | 29 | local slide_styles = {} 30 | 31 | function blank_column(_content) 32 | return { pandoc.RawBlock("html", " ") } 33 | end 34 | 35 | function header_column_narrow_light(c) 36 | local result = pandoc.List({}) 37 | local title = pandoc.utils.stringify(c[1].content) 38 | result:insert(pandoc.RawBlock("html", "

" .. title .. "

")) 39 | return result 40 | end 41 | 42 | function header_column_narrow_dark(c) 43 | local result = pandoc.List({}) 44 | local title = pandoc.utils.stringify(c[1].content) 45 | result:insert(pandoc.RawBlock("html", "

" .. title .. "

")) 46 | return result 47 | end 48 | 49 | function content_column_wide(c) 50 | local result = pandoc.List({}) 51 | local title = pandoc.utils.stringify(c[1].content) 52 | result:extend(c) 53 | result:remove(1) 54 | return result 55 | end 56 | 57 | function content_column(c) 58 | local result = pandoc.List({}) 59 | local title = pandoc.utils.stringify(c[1].content) 60 | result:insert(pandoc.RawBlock("html", "

" .. title .. "

")) 61 | result:extend(c) 62 | result:remove(2) 63 | return result 64 | end 65 | 66 | function bracket_content(c) 67 | local result = pandoc.List({}) 68 | local title = pandoc.utils.stringify(c[1].content) 69 | result:insert(pandoc.RawBlock("html", "

" .. title .. "

")) 70 | result:extend(c) 71 | result:remove(2) 72 | return result 73 | end 74 | 75 | local blank = pandoc.RawBlock("html", " ") 76 | for i, v in ipairs({ 77 | { image = "30-70-dark", 78 | dark = true, 79 | widths = { "35%", "65%" }, 80 | columns = { header_column_narrow_light, content_column_wide }, 81 | }, 82 | { image = "30-70-light", 83 | widths = { "35%", "65%" }, 84 | columns = { header_column_narrow_dark, content_column_wide }, 85 | }, 86 | { image = "30-70-white", 87 | widths = { "35%", "65%" }, 88 | columns = { header_column_narrow_dark, content_column_wide }, 89 | }, 90 | { image = "brackets-dark", 91 | dark = true, 92 | widths = { "100%" }, 93 | columns = { bracket_content }, 94 | }, 95 | { image = "brackets-light", 96 | widths = { "100%" }, 97 | columns = { bracket_content }, 98 | }, 99 | { image = "content-dark", 100 | dark = true, 101 | widths = { "100%" }, 102 | columns = { content_column }, 103 | }, 104 | { image = "content-light", 105 | widths = { "100%" }, 106 | columns = { content_column }, 107 | }, 108 | { image = "content2-light", 109 | widths = { "100%" }, 110 | columns = { content_column }, 111 | }, 112 | { image = "toc-dark", 113 | dark = true, 114 | widths = { "100%" }, 115 | columns = { content_column }, 116 | }, 117 | { image = "toc-light", 118 | widths = { "100%" }, 119 | columns = { content_column }, 120 | }, 121 | { image = "toc-people-dark", 122 | dark = true, 123 | widths = { "100%" }, 124 | columns = { content_column }, 125 | }, 126 | { image = "toc-people-light", 127 | widths = { "100%" }, 128 | columns = { content_column }, 129 | }, 130 | { image = "toc2-dark", 131 | dark = true, 132 | widths = { "100%" }, 133 | columns = { content_column }, 134 | }, 135 | { image = "toc2-light", 136 | widths = { "100%" }, 137 | columns = { content_column }, 138 | }}) do 139 | slide_styles[v.image] = (function(v) 140 | setmetatable(v.columns, getmetatable(pandoc.List({}))) 141 | return function(content) 142 | return many_columns({ 143 | dark = v.dark, 144 | image = v.image, 145 | widths = v.widths, 146 | columns = v.columns:map(function(col) return col(content) end) 147 | }) 148 | end 149 | end)(v) 150 | end 151 | 152 | -- TODO dark-section needs light text 153 | 154 | function mysplit (inputstr, pattern) 155 | if sep == nil then 156 | sep = "%s" 157 | end 158 | local t = {} 159 | local matched = false 160 | for str in string.gmatch(inputstr, pattern) do 161 | matched = true 162 | table.insert(t, str) 163 | end 164 | if not matched then 165 | return { inputstr } 166 | end 167 | return t 168 | end 169 | 170 | function process_header_attributes(header) 171 | local title = pandoc.utils.stringify(header.content) 172 | local match = title:match(" {.*}$") 173 | if match == nil then return header end 174 | local attribute = match:sub(3, -2) 175 | header.content = pandoc.Plain({ pandoc.Str(title:gsub(" {.*}$", "")) }) 176 | for i, v in pairs(mysplit(attribute, " ")) do 177 | if v:sub(1, 1) == "#" then 178 | header.attr.identifier = v:sub(2) 179 | elseif v:sub(1, 1) == "." then 180 | table.insert(header.attr.classes, v:sub(2)) 181 | else 182 | local key, value = i:match("(.*)=(.*)") 183 | if key and value then 184 | header.attr.attributes[key] = value 185 | else 186 | header.attr.attributes[i] = true 187 | end 188 | end 189 | end 190 | return header 191 | end 192 | 193 | 194 | function process(content, classes) 195 | if classes == nil then 196 | classes = content[1].attr.classes 197 | end 198 | for i, v in ipairs(classes) do 199 | if slide_styles[v] then 200 | return slide_styles[v](content) 201 | end 202 | end 203 | return content 204 | end 205 | 206 | function Pandoc(doc) 207 | quarto.doc.add_html_dependency({ 208 | name = 'posit_slides', 209 | scripts = { 'assets/posit-slides.js' }, 210 | }) 211 | 212 | local new_blocks = pandoc.List({}) 213 | local sections = pandoc.utils.make_sections(false, nil, doc.blocks) 214 | 215 | for i, div in ipairs(sections) do 216 | if div.t ~= "Div" then 217 | new_blocks:insert(div) 218 | else 219 | if div.content[1].t == "Header" then 220 | div.content[1] = process_header_attributes(div.content[1]) 221 | end 222 | new_blocks:extend(process(div.content)) 223 | end 224 | end 225 | doc.blocks = new_blocks 226 | return doc 227 | end 228 | -------------------------------------------------------------------------------- /_extensions/quarto-ext/shinylive/README.md: -------------------------------------------------------------------------------- 1 | # Shinylive package methods 2 | 3 | ## Methods 4 | 5 | ### R 6 | 7 | Interaction: 8 | 9 | ``` 10 | Rscript -e 'shinylive:::quarto_ext()' [methods] [args] 11 | ``` 12 | 13 | ### Python 14 | 15 | Interaction: 16 | 17 | ``` 18 | shinylive [methods] [args] 19 | ``` 20 | 21 | ## CLI Methods 22 | 23 | * `extension info` 24 | * Package, version, asset version, and script paths information 25 | * `extension base-htmldeps` 26 | * Quarto html dependencies for the base shinylive integration 27 | * `extension language-resources` 28 | * Language specific resource files for the quarto html dependency named `shinylive` 29 | * `extension app-resources` 30 | * App specific resource files for the quarto html dependency named `shinylive` 31 | 32 | ### CLI Interface 33 | * `extension info` 34 | * Prints information about the extension including: 35 | * `version`: The version of the R package 36 | * `assets_version`: The version of the web assets 37 | * `scripts`: A list of paths scripts that are used by the extension, 38 | mainly `codeblock-to-json` 39 | * Example 40 | ``` 41 | { 42 | "version": "0.1.0", 43 | "assets_version": "0.2.0", 44 | "scripts": { 45 | "codeblock-to-json": "//shinylive-0.2.0/scripts/codeblock-to-json.js" 46 | } 47 | } 48 | ``` 49 | * `extension base-htmldeps` 50 | * Prints the language agnostic quarto html dependencies as a JSON array. 51 | * The first html dependency is the `shinylive` service workers. 52 | * The second html dependency is the `shinylive` base dependencies. This 53 | dependency will contain the core `shinylive` asset scripts (JS files 54 | automatically sourced), stylesheets (CSS files that are automatically 55 | included), and resources (additional files that the JS and CSS files can 56 | source). 57 | * Example 58 | ``` 59 | [ 60 | { 61 | "name": "shinylive-serviceworker", 62 | "version": "0.2.0", 63 | "meta": { "shinylive:serviceworker_dir": "." }, 64 | "serviceworkers": [ 65 | { 66 | "source": "//shinylive-0.2.0/shinylive-sw.js", 67 | "destination": "/shinylive-sw.js" 68 | } 69 | ] 70 | }, 71 | { 72 | "name": "shinylive", 73 | "version": "0.2.0", 74 | "scripts": [{ 75 | "name": "shinylive/load-shinylive-sw.js", 76 | "path": "//shinylive-0.2.0/shinylive/load-shinylive-sw.js", 77 | "attribs": { "type": "module" } 78 | }], 79 | "stylesheets": [{ 80 | "name": "shinylive/shinylive.css", 81 | "path": "//shinylive-0.2.0/shinylive/shinylive.css" 82 | }], 83 | "resources": [ 84 | { 85 | "name": "shinylive/shinylive.js", 86 | "path": "//shinylive-0.2.0/shinylive/shinylive.js" 87 | }, 88 | ... # [ truncated ] 89 | ] 90 | } 91 | ] 92 | ``` 93 | * `extension language-resources` 94 | * Prints the language-specific resource files as JSON that should be added to the quarto html dependency. 95 | * For r-shinylive, this includes the webr resource files 96 | * For py-shinylive, this includes the pyodide and pyright resource files. 97 | * Example 98 | ``` 99 | [ 100 | { 101 | "name": "shinylive/webr/esbuild.d.ts", 102 | "path": "//shinylive-0.2.0/shinylive/webr/esbuild.d.ts" 103 | }, 104 | { 105 | "name": "shinylive/webr/libRblas.so", 106 | "path": "//shinylive-0.2.0/shinylive/webr/libRblas.so" 107 | }, 108 | ... # [ truncated ] 109 | ] 110 | * `extension app-resources` 111 | * Prints app-specific resource files as JSON that should be added to the `"shinylive"` quarto html dependency. 112 | * Currently, r-shinylive does not return any resource files. 113 | * Example 114 | ``` 115 | [ 116 | { 117 | "name": "shinylive/pyodide/anyio-3.7.0-py3-none-any.whl", 118 | "path": "//shinylive-0.2.0/shinylive/pyodide/anyio-3.7.0-py3-none-any.whl" 119 | }, 120 | { 121 | "name": "shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl", 122 | "path": "//shinylive-0.2.0/shinylive/pyodide/appdirs-1.4.4-py2.py3-none-any.whl" 123 | }, 124 | ... # [ truncated ] 125 | ] 126 | ``` 127 | -------------------------------------------------------------------------------- /_extensions/quarto-ext/shinylive/_extension.yml: -------------------------------------------------------------------------------- 1 | name: shinylive 2 | title: Embedded Shinylive applications 3 | author: Winston Chang 4 | version: 0.1.0 5 | quarto-required: ">=1.2.198" 6 | contributes: 7 | filters: 8 | - shinylive.lua 9 | -------------------------------------------------------------------------------- /_extensions/quarto-ext/shinylive/resources/css/shinylive-quarto.css: -------------------------------------------------------------------------------- 1 | div.output-content, 2 | div.shinylive-wrapper { 3 | background-color: rgba(250, 250, 250, 0.65); 4 | border: 1px solid rgba(233, 236, 239, 0.65); 5 | border-radius: 0.5rem; 6 | box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.04), 0px 3px 7px rgba(0, 0, 0, 0.04), 7 | 0px 12px 30px rgba(0, 0, 0, 0.07); 8 | margin-top: 32px; 9 | margin-bottom: 32px; 10 | } 11 | 12 | div.shinylive-wrapper { 13 | margin: 1em 0; 14 | border-radius: 8px; 15 | } 16 | 17 | .shinylive-container { 18 | background-color: #eeeff2; 19 | min-height: auto; 20 | } 21 | 22 | .shinylive-container > div { 23 | box-shadow: none; 24 | } 25 | 26 | .editor-container .cm-editor .cm-scroller { 27 | font-size: 13px; 28 | line-height: 1.5; 29 | } 30 | 31 | iframe.app-frame { 32 | /* Override the default margin from Bootstrap */ 33 | margin-bottom: 0; 34 | } 35 | -------------------------------------------------------------------------------- /_extensions/shafayetShafee/bsicons/_extension.yml: -------------------------------------------------------------------------------- 1 | title: Support for Bootstrap Icons 2 | author: Shafayet Khan Shafee 3 | version: 1.0.0 4 | quarto-required: ">=1.2.0" 5 | contributes: 6 | shortcodes: 7 | - bsicons.lua 8 | 9 | -------------------------------------------------------------------------------- /_extensions/shafayetShafee/bsicons/assets/webfonts/bootstrap-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/_extensions/shafayetShafee/bsicons/assets/webfonts/bootstrap-icons.woff -------------------------------------------------------------------------------- /_extensions/shafayetShafee/bsicons/assets/webfonts/bootstrap-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/_extensions/shafayetShafee/bsicons/assets/webfonts/bootstrap-icons.woff2 -------------------------------------------------------------------------------- /_extensions/shafayetShafee/bsicons/bsicons.lua: -------------------------------------------------------------------------------- 1 | local function ensureHtmlDeps() 2 | quarto.doc.addHtmlDependency({ 3 | name = "bootstrap-icons", 4 | version = "1.9.1", 5 | stylesheets = {"assets/css/all.css"} 6 | }) 7 | end 8 | 9 | local function isEmpty(s) 10 | return s == nil or s == '' 11 | end 12 | 13 | local str = pandoc.utils.stringify 14 | 15 | return { 16 | ["bi"] = function(args, kwargs) 17 | local icon = str(args[1]) 18 | local size = str(kwargs["size"]) 19 | local color = str(kwargs["color"]) 20 | local label = str(kwargs["label"]) 21 | local class = str(kwargs["class"]) 22 | 23 | if not isEmpty(size) then 24 | size = "font-size: " .. size .. ";" 25 | else 26 | size = '' 27 | end 28 | 29 | if not isEmpty(color) then 30 | color = "color: " .. color .. ";" 31 | else 32 | color = '' 33 | end 34 | 35 | local style = "style=\"" .. size .. color .. "\"" 36 | 37 | if not isEmpty(label) then 38 | label = " aria-label=\"" .. label .. "\"" 39 | end 40 | 41 | if isEmpty(class) then 42 | class = '' 43 | end 44 | 45 | local role = "role=\"img\"" 46 | local aria_hidden = "aria-hidden=\"true\"" 47 | 48 | if quarto.doc.isFormat("html:js") then 49 | ensureHtmlDeps() 50 | if isEmpty(label) then 51 | return pandoc.RawInline( 52 | 'html', 53 | "" 54 | ) 55 | else 56 | return pandoc.RawInline( 57 | 'html', 58 | "" 59 | ) 60 | end 61 | else 62 | return pandoc.Null() 63 | end 64 | 65 | end 66 | } -------------------------------------------------------------------------------- /_publish.yml: -------------------------------------------------------------------------------- 1 | - source: project 2 | quarto-pub: 3 | - id: 95f693d8-9af0-46bc-aa91-883fc946d55d 4 | url: 'https://gordon.quarto.pub/shiny-for-python-workshop' 5 | -------------------------------------------------------------------------------- /_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | 4 | website: 5 | title: "Shiny for Python Workshop" 6 | sidebar: 7 | style: docked 8 | contents: 9 | - section: "Exercises" 10 | contents: 11 | - exercises/1-hello-world.qmd 12 | - exercises/2-basic-ui.qmd 13 | - exercises/3-reactivity.qmd 14 | - exercises/5-reactive-effect.qmd 15 | - exercises/4-dynamic-ui.qmd 16 | 17 | navbar: 18 | tools: 19 | - icon: github 20 | href: https://github.com/talkpython/reactive-web-dashboards-with-shiny-course 21 | left: 22 | - href: index.qmd 23 | text: Home 24 | 25 | filters: 26 | - shinylive 27 | 28 | format: 29 | html: 30 | theme: cosmo 31 | margin-right: 0px 32 | css: styles.css 33 | toc: true 34 | toc-location: left 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /apps/examples/0.0-penguins/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pandas as pd 4 | import seaborn as sns 5 | 6 | import shiny.experimental as x 7 | from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui 8 | 9 | sns.set_theme() 10 | 11 | # https://raw.githubusercontent.com/jcheng5/simplepenguins.R/main/penguins.csv 12 | 13 | df = pd.read_csv(Path(__file__).parent / "penguins.csv", na_values="NA") 14 | numeric_cols = df.select_dtypes(include=["float64"]).columns.tolist() 15 | species = df["Species"].unique().tolist() 16 | species.sort() 17 | 18 | app_ui = x.ui.page_sidebar( 19 | x.ui.sidebar( 20 | ui.input_selectize( 21 | "xvar", "X variable", numeric_cols, selected="Bill Length (mm)" 22 | ), 23 | ui.input_selectize( 24 | "yvar", "Y variable", numeric_cols, selected="Bill Depth (mm)" 25 | ), 26 | ui.input_checkbox_group( 27 | "species", "Filter by species", species, selected=species 28 | ), 29 | ui.hr(), 30 | ui.input_switch("by_species", "Show species", value=True), 31 | ui.input_switch("show_margins", "Show marginal plots", value=True), 32 | ), 33 | x.ui.output_plot("scatter"), 34 | ) 35 | 36 | 37 | def server(input: Inputs, output: Outputs, session: Session): 38 | @reactive.Calc 39 | def filtered_df() -> pd.DataFrame: 40 | """Returns a Pandas data frame that includes only the desired rows""" 41 | 42 | # This calculation "req"uires that at least one species is selected 43 | req(len(input.species()) > 0) 44 | 45 | # Filter the rows so we only include the desired species 46 | return df[df["Species"].isin(input.species())] 47 | 48 | @output 49 | @render.plot 50 | def scatter(): 51 | """Generates a plot for Shiny to display to the user""" 52 | 53 | # The plotting function to use depends on whether margins are desired 54 | plotfunc = sns.jointplot if input.show_margins() else sns.scatterplot 55 | 56 | plotfunc( 57 | data=filtered_df(), 58 | x=input.xvar(), 59 | y=input.yvar(), 60 | hue="Species" if input.by_species() else None, 61 | hue_order=species, 62 | legend=False, 63 | ) 64 | 65 | 66 | app = App(app_ui, server) 67 | -------------------------------------------------------------------------------- /apps/examples/0.0-penguins/requirements.txt: -------------------------------------------------------------------------------- 1 | shiny==0.5.1 2 | shinyswatch==0.2.4 3 | seaborn==0.12.2 4 | matplotlib==3.7.1 -------------------------------------------------------------------------------- /apps/examples/1.0-basic-app/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from shiny import App, render, ui, reactive 4 | 5 | app_ui = ui.page_fluid( 6 | ui.input_radio_buttons( 7 | "command_choice", "Choose a command", choices=["Option 1", "Option 2"] 8 | ), 9 | ui.output_ui("dynamic_text"), 10 | ui.input_action_button("compute_button", "COMPUTE"), 11 | ) 12 | 13 | 14 | def server(input, output, session): 15 | @output 16 | @render.ui 17 | def dynamic_text(): 18 | if input.command_choice() == "Option 1": 19 | id = "output_text_1" 20 | else: 21 | id = "output_text_2" 22 | return ui.output_text(id) 23 | 24 | @output 25 | @render.text 26 | @reactive.event(input.compute_button) 27 | def output_text_1(): 28 | return "Result from option 1" 29 | 30 | @output 31 | @render.text 32 | @reactive.event(input.compute_button) 33 | def output_text_2(): 34 | return "Result from option 2" 35 | 36 | 37 | app = App(app_ui, server) 38 | -------------------------------------------------------------------------------- /apps/examples/1.1-table-only/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "penguins.csv" 6 | penguins = pd.read_csv(infile) 7 | 8 | app_ui = ui.page_fluid( 9 | ui.h2("Hello Penguins!"), 10 | ui.output_data_frame("table"), 11 | ) 12 | 13 | 14 | def server(input, output, session): 15 | @output 16 | @render.data_frame 17 | def table(): 18 | return penguins 19 | 20 | 21 | app = App(app_ui, server) 22 | -------------------------------------------------------------------------------- /apps/examples/1.2-table-summary/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "penguins.csv" 6 | penguins = pd.read_csv(infile) 7 | 8 | app_ui = ui.page_fluid( 9 | ui.h2("Hello Penguins!"), 10 | ui.output_data_frame("table"), 11 | ) 12 | 13 | 14 | def server(input, output, session): 15 | @output 16 | @render.data_frame 17 | def table(): 18 | summary = ( 19 | penguins.copy() 20 | .set_index("species") 21 | .groupby(level="species") 22 | .agg({"bill_length": "mean", "bill_depth": "mean"}) 23 | .reset_index() 24 | ) 25 | return summary 26 | 27 | 28 | app = App(app_ui, server) 29 | -------------------------------------------------------------------------------- /apps/examples/1.3-table-input-slider/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "penguins.csv" 6 | penguins = pd.read_csv(infile) 7 | 8 | app_ui = ui.page_fluid( 9 | ui.h2("Hello Penguins!"), 10 | ui.input_slider( 11 | "mass", 12 | "Mass", 13 | 2000, 14 | 8000, 15 | 6000, 16 | ), 17 | ui.output_data_frame("table"), 18 | ) 19 | 20 | 21 | def server(input, output, session): 22 | @output 23 | @render.data_frame 24 | def table(): 25 | df = penguins.copy() 26 | filtered = df.loc[df["body_mass"] < input.mass()] 27 | summary = ( 28 | filtered.set_index("species") 29 | .groupby(level="species") 30 | .agg({"bill_length": "mean", "bill_depth": "mean"}) 31 | .reset_index() 32 | ) 33 | return summary 34 | 35 | 36 | app = App(app_ui, server) 37 | -------------------------------------------------------------------------------- /apps/examples/1.4-table-graph-input-slider/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import dist_plot 5 | 6 | infile = Path(__file__).parent / "penguins.csv" 7 | penguins = pd.read_csv(infile) 8 | 9 | app_ui = ui.page_fluid( 10 | ui.h2("Hello Penguins!"), 11 | ui.input_slider( 12 | "mass", 13 | "Mass", 14 | 2000, 15 | 8000, 16 | 6000, 17 | ), 18 | ui.output_data_frame("table"), 19 | ui.output_plot("dist"), 20 | ) 21 | 22 | 23 | def server(input, output, session): 24 | @output 25 | @render.data_frame 26 | def table(): 27 | df = penguins.copy() 28 | filtered = df.loc[df["body_mass"] < input.mass()] 29 | summary = ( 30 | filtered.set_index("species") 31 | .groupby(level="species") 32 | .agg({"bill_length": "mean", "bill_depth": "mean"}) 33 | .reset_index() 34 | ) 35 | return summary 36 | 37 | @output 38 | @render.plot 39 | def dist(): 40 | df = penguins.copy() 41 | filtered = df.loc[df["body_mass"] < input.mass()] 42 | return dist_plot(filtered) 43 | 44 | 45 | app = App(app_ui, server) 46 | -------------------------------------------------------------------------------- /apps/examples/1.4-table-graph-input-slider/plots.py: -------------------------------------------------------------------------------- 1 | from plotnine import ggplot, geom_density, aes, theme_light, geom_point, stat_smooth 2 | 3 | 4 | def dist_plot(df): 5 | plot = ( 6 | ggplot(df, aes(x="body_mass", fill="species")) 7 | + geom_density(alpha=0.2) 8 | + theme_light() 9 | ) 10 | return plot 11 | 12 | 13 | def scatter_plot(df, trend_line=False): 14 | plot = ( 15 | ggplot( 16 | df, 17 | aes( 18 | x="bill_length", 19 | y="bill_depth", 20 | color="species", 21 | group="species", 22 | ), 23 | ) 24 | + geom_point() 25 | + theme_light() 26 | ) 27 | 28 | if trend_line: 29 | plot = plot + stat_smooth() 30 | 31 | return plot 32 | -------------------------------------------------------------------------------- /apps/examples/2.0-simple-reactive-calc/app.py: -------------------------------------------------------------------------------- 1 | from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui 2 | import pandas as pd 3 | from plotnine import ggplot, geom_histogram, labs, aes 4 | import numpy as np 5 | 6 | app_ui = ui.page_fluid( 7 | ui.input_slider("n_rows", "Sample rows", 0, 100, 20), 8 | ui.row(ui.column(4, ui.output_table("df")), ui.column(8, ui.output_plot("hist"))), 9 | ) 10 | 11 | 12 | def server(input: Inputs, output: Outputs, session: Session): 13 | @output 14 | @render.table 15 | def df(): 16 | rand = np.random.rand(input.n_rows(), 1) 17 | df = pd.DataFrame(rand, columns=["col_1"]) 18 | return df 19 | 20 | @output 21 | @render.plot 22 | def hist(): 23 | rand = np.random.rand(input.n_rows(), 1) 24 | df = pd.DataFrame(rand, columns=["col_1"]) 25 | plot = ( 26 | ggplot(df, aes(x="col_1")) 27 | + geom_histogram(binwidth=0.1, fill="blue", color="black") 28 | + labs(x="Random Values", y="Frequency", title="Histogram of Random Data") 29 | ) 30 | return plot 31 | 32 | 33 | app = App(app_ui, server) 34 | -------------------------------------------------------------------------------- /apps/examples/2.1-reactive-calc/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui, reactive 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import dist_plot, scatter_plot 5 | 6 | infile = Path(__file__).parent / "penguins.csv" 7 | penguins = pd.read_csv(infile) 8 | 9 | app_ui = ui.page_fluid( 10 | ui.h2("Hello Penguins!"), 11 | ui.input_slider( 12 | "mass", 13 | "Mass", 14 | 2000, 15 | 8000, 16 | 6000, 17 | ), 18 | ui.output_data_frame("table"), 19 | ui.output_plot("dist"), 20 | ui.input_checkbox("trend", "Add trendline"), 21 | ui.output_plot("scatter"), 22 | ) 23 | 24 | 25 | def server(input, output, session): 26 | @reactive.Calc 27 | def filt_df(): 28 | df = penguins.copy() 29 | filtered = df.loc[df["body_mass"] < input.mass()] 30 | return filtered 31 | 32 | @output 33 | @render.data_frame 34 | def table(): 35 | summary = ( 36 | filt_df() 37 | .set_index("species") 38 | .groupby(level="species") 39 | .agg({"bill_length": "mean", "bill_depth": "mean"}) 40 | .reset_index() 41 | ) 42 | return summary 43 | 44 | @output 45 | @render.plot 46 | def dist(): 47 | return dist_plot(filt_df()) 48 | 49 | @output 50 | @render.plot 51 | def scatter(): 52 | return scatter_plot(filt_df(), input.trend()) 53 | 54 | 55 | app = App(app_ui, server) 56 | -------------------------------------------------------------------------------- /apps/examples/2.1-reactive-calc/plots.py: -------------------------------------------------------------------------------- 1 | from plotnine import ggplot, geom_density, aes, theme_light, geom_point, stat_smooth 2 | 3 | 4 | def dist_plot(df): 5 | plot = ( 6 | ggplot(df, aes(x="body_mass", fill="species")) 7 | + geom_density(alpha=0.2) 8 | + theme_light() 9 | ) 10 | return plot 11 | 12 | 13 | def scatter_plot(df, trend_line=False): 14 | plot = ( 15 | ggplot( 16 | df, 17 | aes( 18 | x="bill_length", 19 | y="bill_depth", 20 | color="species", 21 | group="species", 22 | ), 23 | ) 24 | + geom_point() 25 | + theme_light() 26 | ) 27 | 28 | if trend_line: 29 | plot = plot + stat_smooth() 30 | 31 | return plot 32 | -------------------------------------------------------------------------------- /apps/examples/3.0-modal/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, Inputs, Outputs, Session, reactive, ui, render 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_action_button("show", "Show modal dialog"), ui.output_text("txt") 5 | ) 6 | 7 | 8 | def server(input: Inputs, output: Outputs, session: Session): 9 | @reactive.Effect 10 | @reactive.event(input.show) 11 | def show_modal(): 12 | m = ui.modal( 13 | "This is a somewhat important message.", 14 | title="Click outside the modal to close", 15 | easy_close=True, 16 | footer=None, 17 | ) 18 | ui.modal_show(m) 19 | 20 | 21 | app = App(app_ui, server) 22 | -------------------------------------------------------------------------------- /apps/examples/3.3-css/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from shiny import App, Inputs, Outputs, Session, render, ui 4 | 5 | css_path = Path(__file__).parent / "www" / "my_styles.css" 6 | 7 | app_ui = ui.page_fluid( 8 | {"class": "blue-body"}, 9 | ui.include_css(css_path), 10 | ui.input_slider("num", "Number:", min=10, max=100, value=30), 11 | ui.div( 12 | {"class": "bold-output;"}, 13 | ui.output_text("slider_val"), 14 | ), 15 | ) 16 | 17 | 18 | def server(input: Inputs, output: Outputs, session: Session): 19 | @output 20 | @render.text 21 | def slider_val(): 22 | return f"{input.num()}" 23 | 24 | 25 | app = App(app_ui, server) 26 | -------------------------------------------------------------------------------- /apps/examples/3.3-css/www/my_styles.css: -------------------------------------------------------------------------------- 1 | .blue-body { 2 | background-color: rgba(0, 128, 255, 0.5); 3 | } 4 | 5 | .bold-output { 6 | font-weight: bold; 7 | } -------------------------------------------------------------------------------- /apps/examples/4.0.0-navsets/app.py: -------------------------------------------------------------------------------- 1 | from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.navset_tab( 5 | ui.nav( 6 | "Tab1", 7 | ui.input_slider("slider", "Slider", 0, 100, 20), 8 | ), 9 | ui.nav("Tab2", ui.input_action_button("button", "Button")), 10 | ui.nav("Tab3", ui.input_action_button("button2", "Button 2")), 11 | ) 12 | ) 13 | 14 | app = App(app_ui, None) 15 | -------------------------------------------------------------------------------- /apps/examples/4.0.0.0-cards/app.py: -------------------------------------------------------------------------------- 1 | from shiny import Inputs, Outputs, Session, App, reactive, render, req, ui 2 | import shiny.experimental as x 3 | 4 | app_ui = ui.page_fluid( 5 | ui.column( 6 | 3, 7 | x.ui.card( 8 | x.ui.card_header("Slider card"), 9 | ui.input_slider("n", "N", 0, 100, 20), 10 | ui.output_text_verbatim("txt"), 11 | fill=False, 12 | ), 13 | ) 14 | ) 15 | 16 | 17 | def server(input: Inputs, output: Outputs, session: Session): 18 | @output 19 | @render.text 20 | def txt(): 21 | return f"n*2 is {input.n() * 2}" 22 | 23 | 24 | app = App(app_ui, server) 25 | -------------------------------------------------------------------------------- /apps/examples/4.0.1-rows-columns/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, ui 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | style = "border: 1px solid #999;" 6 | 7 | app_ui = ui.page_fluid( 8 | ui.row( 9 | ui.column(4, "row-1 col-1", style=style), 10 | ui.column(4, "row-1 col-2", style=style), 11 | ui.column(4, "row-1 col-3", style=style), 12 | ), 13 | ui.row( 14 | ui.column(6, "row-2 col-1", style=style), 15 | ui.column(6, "row-2 col-2", style=style), 16 | ), 17 | ) 18 | 19 | 20 | app = App(app_ui, None) 21 | -------------------------------------------------------------------------------- /apps/examples/4.0.2-nested-columns/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, ui 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | style = "border: 1px solid #999;" 6 | 7 | app_ui = ui.page_fluid( 8 | ui.row( 9 | ui.column( 10 | 6, 11 | "row-1 col-2", 12 | ui.row( 13 | ui.column(4, "nest-1", style=style), 14 | ui.column(4, "nest-2", style=style), 15 | ui.column(4, "nest-2", style=style), 16 | ), 17 | style=style, 18 | ), 19 | ui.column(4, "row-1 col-2", style=style), 20 | ), 21 | ) 22 | 23 | 24 | app = App(app_ui, None) 25 | -------------------------------------------------------------------------------- /apps/examples/4.1.1-dynamic-ui/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_checkbox("show_checkbox", "Show Checkbox"), 5 | ui.panel_conditional( 6 | "input.show_checkbox", 7 | ui.input_checkbox("show_slider", "Show Slider"), 8 | ), 9 | ui.output_ui("dynamic_slider"), 10 | ) 11 | 12 | 13 | def server(input, output, session): 14 | @output 15 | @render.ui 16 | def dynamic_slider(): 17 | print(input.show_slider()) 18 | if input.show_slider(): 19 | return ui.input_slider("n", "N", 0, 100, 20) 20 | 21 | 22 | app = App(app_ui, server) 23 | -------------------------------------------------------------------------------- /apps/examples/4.1.2-conditional-panel/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_checkbox("show_slider", "Show Slider"), 5 | ui.panel_conditional( 6 | "input.show_slider", ui.input_slider("slider", "Slider", 0, 100, 50) 7 | ), 8 | ) 9 | 10 | app = App(app_ui, None) 11 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.0-hello-world/README: -------------------------------------------------------------------------------- 1 | Modify the app to add "Hello World!" to the main heading. 2 | Click the play button to run the app. -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.0-hello-world/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import ui 2 | 3 | ui.h1("Hello World!") 4 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.0-hello-world/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import ui 2 | 3 | ui.h1("") 4 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.0-hello-world/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.1-data-frame/README: -------------------------------------------------------------------------------- 1 | Add the right `@render` decorator to the `penguins_df` function to get the data frame to render. -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.1-data-frame/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import ui, render 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "simulated-data.csv" 6 | df = pd.read_csv(infile) 7 | df = df.drop(columns=["text"]) 8 | 9 | # The render.data_frame decorator is used to display dataframes. 10 | 11 | 12 | @render.data_frame 13 | def penguins_df(): 14 | return df 15 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.1-data-frame/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import ui, render 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "simulated-data.csv" 6 | df = pd.read_csv(infile) 7 | df = df.drop(columns=["text"]) 8 | 9 | 10 | @render.____ 11 | def penguins_df(): 12 | return df 13 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.1-data-frame/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.2-debug/README: -------------------------------------------------------------------------------- 1 | This app is not rendering properly, can you figure out what's wrong? -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.2-debug/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import ui, render 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "model_data.csv" 6 | df = pd.read_csv(infile) 7 | df = df.drop(columns=["text"]) 8 | 9 | 10 | # It's important that you use the right decorator for your funciton's return value 11 | # in this case since the function is returning a dataframe we need to use the 12 | # `@render.data_frame` decorator. 13 | @render.data_frame 14 | def penguins_df(): 15 | return df 16 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.2-debug/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import ui, render 2 | import pandas as pd 3 | from pathlib import Path 4 | 5 | infile = Path(__file__).parent / "model_data.csv" 6 | df = pd.read_csv(infile) 7 | df = df.drop(columns=["text"]) 8 | 9 | 10 | @render.plot 11 | def penguins_df(): 12 | return df 13 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.2-debug/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.3-filter-input/README: -------------------------------------------------------------------------------- 1 | Add a select input to the app which lets the user select one of the accounts. For reference the account list is: 2 | `["Berge & Berge", "Fritsch & Fritsch", "Hintz & Hintz", "Mosciski and Sons", "Wolff Ltd"]` -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.3-filter-input/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | # All the inputs start with `ui.input_*`, which adds an input to the app. 7 | # Note that this input doesn't do anything because we haven't connected it 8 | # to a rendering function. 9 | 10 | ui.input_select( 11 | id="account", 12 | label="Account", 13 | choices=[ 14 | "Berge & Berge", 15 | "Fritsch & Fritsch", 16 | "Hintz & Hintz", 17 | "Mosciski and Sons", 18 | "Wolff Ltd", 19 | ], 20 | ) 21 | 22 | 23 | @render.data_frame 24 | def table(): 25 | account_counts = df.groupby("account").size().reset_index(name="counts") 26 | return account_counts 27 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.3-filter-input/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | 7 | @render.data_frame 8 | def table(): 9 | account_counts = df.groupby("account").size().reset_index(name="counts") 10 | return account_counts 11 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.3-filter-input/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | 4 | file_path = Path(__file__).parent / "simulated-data.csv" 5 | 6 | print("Running!") 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.3-filter-input/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.4-filter-connect/README: -------------------------------------------------------------------------------- 1 | The select input is not connected to the output table so changing it doesn't do anything. Modify the app so that the data frame is filtered when the selection changes. -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.4-filter-connect/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | ui.input_select( 7 | id="account", 8 | label="Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render.data_frame 20 | def table(): 21 | # When we call the account input with `input.account()` we can use its value 22 | # with regular Python code. This will also cause the rendering function 23 | # to rerun whenever the user changes the account value. 24 | account_subset = df[df["account"] == input.account()] 25 | account_counts = ( 26 | account_subset.groupby(["account", "sub_account"]) 27 | .size() 28 | .reset_index(name="count") 29 | ) 30 | return account_counts 31 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.4-filter-connect/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | ui.input_select( 7 | id="account", 8 | label="Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render.data_frame 20 | def table(): 21 | account_subset = df 22 | account_counts = ( 23 | account_subset.groupby(["account", "sub_account"]) 24 | .size() 25 | .reset_index(name="count") 26 | ) 27 | return account_counts 28 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.4-filter-connect/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.4-filter-connect/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.5-debug/README: -------------------------------------------------------------------------------- 1 | Something is wrong with this application, can you fix it? -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.5-debug/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render.data_frame 20 | def table(): 21 | # Remember that inputs are callable values, so whenever you refer to them 22 | # in rendering functions you need to call them (`input.account()` not `input.account`) 23 | account_subset = df[df["account"] == input.account()] 24 | account_counts = ( 25 | account_subset.groupby("sub_account").size().reset_index(name="counts") 26 | ) 27 | return account_counts 28 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.5-debug/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render.data_frame 20 | def table(): 21 | account_subset = df[df["account"] == input.account] 22 | account_counts = ( 23 | account_subset.groupby("sub_account").size().reset_index(name="counts") 24 | ) 25 | return account_counts 26 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.5-debug/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.5-debug/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.6-debug/README: -------------------------------------------------------------------------------- 1 | This app is generating the error "No current reactive context", can you resolve it? -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.6-debug/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render.data_frame 20 | def table(): 21 | # Inputs can only be called within a reactive context, which 22 | # means that if you refer to them outside of a rendering function 23 | # you'll get an error. 24 | account_subset = df[df["account"] == input.account()] 25 | account_counts = ( 26 | account_subset.groupby("sub_account").size().reset_index(name="counts") 27 | ) 28 | return account_counts 29 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.6-debug/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | import pandas as pd 3 | from pathlib import Path 4 | from data_import import df 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | account_subset = df[df["account"] == input.account()] 20 | 21 | 22 | @render.data_frame 23 | def table(): 24 | account_counts = ( 25 | account_subset.groupby("sub_account").size().reset_index(name="counts") 26 | ) 27 | return account_counts 28 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.6-debug/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.6-debug/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.7-add-plot/README: -------------------------------------------------------------------------------- 1 | Use the `plot_precision_recall_curve` function from the `plots.py` module to add a second plot output to the app. Note that you should use the `@render_plotly` decorator -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.7-add-plot/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | # It's a good idea to use helper functions for things like drawing plots 19 | # or displaying data frames because it makes your app more modular and easy to 20 | # read. 21 | @render_plotly 22 | def precision_recall_plot(): 23 | account_subset = df[df["account"] == input.account()] 24 | return plot_precision_recall_curve( 25 | account_subset, "is_electronics", "training_score" 26 | ) 27 | 28 | 29 | @render_plotly 30 | def auc_plot(): 31 | account_subset = df[df["account"] == input.account()] 32 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 33 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.7-add-plot/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render_plotly 20 | def auc_plot(): 21 | account_subset = df[df["account"] == input.account()] 22 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 23 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.7-add-plot/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.7-add-plot/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/1.7-add-plot/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/1-getting-started/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.1-sidebar/README: -------------------------------------------------------------------------------- 1 | Use `ui.sidebar()` to put the Account dropdown in a sidebar. -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.1-sidebar/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | # The ui.sidebar can be included anywhere in your app script, 7 | # but by convention it usually goes near the top. 8 | with ui.sidebar(): 9 | ui.input_select( 10 | "account", 11 | "Account", 12 | choices=[ 13 | "Berge & Berge", 14 | "Fritsch & Fritsch", 15 | "Hintz & Hintz", 16 | "Mosciski and Sons", 17 | "Wolff Ltd", 18 | ], 19 | ) 20 | 21 | 22 | @render_plotly 23 | def precision_recall_plot(): 24 | account_subset = df[df["account"] == input.account()] 25 | return plot_precision_recall_curve( 26 | account_subset, "is_electronics", "training_score" 27 | ) 28 | 29 | 30 | @render_plotly 31 | def auc_plot(): 32 | account_subset = df[df["account"] == input.account()] 33 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 34 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.1-sidebar/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | ui.input_select( 7 | "account", 8 | "Account", 9 | choices=[ 10 | "Berge & Berge", 11 | "Fritsch & Fritsch", 12 | "Hintz & Hintz", 13 | "Mosciski and Sons", 14 | "Wolff Ltd", 15 | ], 16 | ) 17 | 18 | 19 | @render_plotly 20 | def precision_recall_plot(): 21 | account_subset = df[df["account"] == input.account()] 22 | return plot_precision_recall_curve( 23 | account_subset, "is_electronics", "training_score" 24 | ) 25 | 26 | 27 | @render_plotly 28 | def auc_plot(): 29 | account_subset = df[df["account"] == input.account()] 30 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 31 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.1-sidebar/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.1-sidebar/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.1-sidebar/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.2-cards/README: -------------------------------------------------------------------------------- 1 | Wrap the two plots in cards. -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.2-cards/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | with ui.sidebar(): 7 | ui.input_select( 8 | "account", 9 | "Account", 10 | choices=[ 11 | "Berge & Berge", 12 | "Fritsch & Fritsch", 13 | "Hintz & Hintz", 14 | "Mosciski and Sons", 15 | "Wolff Ltd", 16 | ], 17 | ) 18 | 19 | 20 | # Adding a ui.card context manager will add a card to the UI. 21 | # This is useful for grouping related elements together. 22 | # You can add as many ui elements as you like to the card. 23 | with ui.card(): 24 | 25 | @render_plotly 26 | def precision_recall_plot(): 27 | account_subset = df[df["account"] == input.account()] 28 | return plot_precision_recall_curve( 29 | account_subset, "is_electronics", "training_score" 30 | ) 31 | 32 | 33 | with ui.card(): 34 | 35 | @render_plotly 36 | def auc_plot(): 37 | account_subset = df[df["account"] == input.account()] 38 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 39 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.2-cards/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | with ui.sidebar(): 7 | ui.input_select( 8 | "account", 9 | "Account", 10 | choices=[ 11 | "Berge & Berge", 12 | "Fritsch & Fritsch", 13 | "Hintz & Hintz", 14 | "Mosciski and Sons", 15 | "Wolff Ltd", 16 | ], 17 | ) 18 | 19 | 20 | @render_plotly 21 | def precision_recall_plot(): 22 | account_subset = df[df["account"] == input.account()] 23 | return plot_precision_recall_curve( 24 | account_subset, "is_electronics", "training_score" 25 | ) 26 | 27 | 28 | @render_plotly 29 | def auc_plot(): 30 | account_subset = df[df["account"] == input.account()] 31 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 32 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.2-cards/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.2-cards/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.2-cards/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.3-cards-switch/README: -------------------------------------------------------------------------------- 1 | Lets add a dropdown to let the user select the model metrics, this involves a few steps: 2 | 1) Add a `ui.input_select` item to the card with two options 3 | 2) Modify the `@render.plotly` to conditionally display one plot or the other based on that dropdown 4 | 5 | You will only need one `@render.plotly` function. -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.3-cards-switch/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | with ui.sidebar(): 7 | ui.input_select( 8 | "account", 9 | "Account", 10 | choices=[ 11 | "Berge & Berge", 12 | "Fritsch & Fritsch", 13 | "Hintz & Hintz", 14 | "Mosciski and Sons", 15 | "Wolff Ltd", 16 | ], 17 | ) 18 | 19 | # Cards are great for grouping related elements together. 20 | with ui.card(): 21 | ui.card_header("Model Metrics") 22 | 23 | @render_plotly 24 | def metric_plot(): 25 | account_subset = df[df["account"] == input.account()] 26 | if input.metric() == "ROC Curve": 27 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 28 | else: 29 | return plot_precision_recall_curve( 30 | account_subset, "is_electronics", "training_score" 31 | ) 32 | 33 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 34 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.3-cards-switch/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve 4 | from shinywidgets import render_plotly 5 | 6 | with ui.sidebar(): 7 | ui.input_select( 8 | "account", 9 | "Account", 10 | choices=[ 11 | "Berge & Berge", 12 | "Fritsch & Fritsch", 13 | "Hintz & Hintz", 14 | "Mosciski and Sons", 15 | "Wolff Ltd", 16 | ], 17 | ) 18 | 19 | with ui.card(): 20 | 21 | @render_plotly 22 | def precision_recall_plot(): 23 | account_subset = df[df["account"] == input.account()] 24 | return plot_precision_recall_curve( 25 | account_subset, "is_electronics", "training_score" 26 | ) 27 | 28 | 29 | with ui.card(): 30 | 31 | @render_plotly 32 | def auc_plot(): 33 | account_subset = df[df["account"] == input.account()] 34 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 35 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.3-cards-switch/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.3-cards-switch/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.3-cards-switch/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.4-layout-columns/README: -------------------------------------------------------------------------------- 1 | Use `ui.layout_columns()` to split the app into two columns. 2 | 3 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.4-layout-columns/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 4 | from shinywidgets import render_plotly 5 | 6 | with ui.sidebar(): 7 | ui.input_select( 8 | "account", 9 | "Account", 10 | choices=[ 11 | "Berge & Berge", 12 | "Fritsch & Fritsch", 13 | "Hintz & Hintz", 14 | "Mosciski and Sons", 15 | "Wolff Ltd", 16 | ], 17 | ) 18 | 19 | # `layout_columns` lays child elements out side by side. 20 | # You can add anything to the layout columns context manager, but it 21 | # works best with cards and value boxes. 22 | with ui.layout_columns(): 23 | 24 | with ui.card(): 25 | ui.card_header("Model Metrics") 26 | 27 | @render_plotly 28 | def metric_plot(): 29 | account_subset = df[df["account"] == input.account()] 30 | if input.metric() == "ROC Curve": 31 | return plot_auc_curve( 32 | account_subset, "is_electronics", "training_score" 33 | ) 34 | else: 35 | return plot_precision_recall_curve( 36 | account_subset, "is_electronics", "training_score" 37 | ) 38 | 39 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 40 | 41 | with ui.card(): 42 | ui.card_header("Model Scores") 43 | 44 | @render_plotly 45 | def score_dist(): 46 | account_subset = df[df["account"] == input.account()] 47 | return plot_score_distribution(account_subset) 48 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.4-layout-columns/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 4 | from shinywidgets import render_plotly 5 | 6 | with ui.sidebar(): 7 | ui.input_select( 8 | "account", 9 | "Account", 10 | choices=[ 11 | "Berge & Berge", 12 | "Fritsch & Fritsch", 13 | "Hintz & Hintz", 14 | "Mosciski and Sons", 15 | "Wolff Ltd", 16 | ], 17 | ) 18 | 19 | with ui.card(): 20 | ui.card_header("Model Metrics") 21 | 22 | @render_plotly 23 | def metric_plot(): 24 | account_subset = df[df["account"] == input.account()] 25 | if input.metric() == "ROC Curve": 26 | return plot_auc_curve(account_subset, "is_electronics", "training_score") 27 | else: 28 | return plot_precision_recall_curve( 29 | account_subset, "is_electronics", "training_score" 30 | ) 31 | 32 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 33 | 34 | 35 | with ui.card(): 36 | ui.card_header("Model Scores") 37 | 38 | @render_plotly 39 | def score_dist(): 40 | account_subset = df[df["account"] == input.account()] 41 | return plot_score_distribution(account_subset) 42 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.4-layout-columns/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.4-layout-columns/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/2.4-layout-columns/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/2-basic-ui/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.1-reactive-calc/README: -------------------------------------------------------------------------------- 1 | Extract the account filtering logic into a reactive calculation. 2 | 3 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.1-reactive-calc/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | # You should always extract repeated logic into a reactive.cacl 9 | # in addition to making your app easier to read it will also 10 | # make it run faster because the reactive calc is calculated once 11 | # even if many other rendering functions retrieve its value. 12 | @reactive.calc 13 | def account_data(): 14 | return df[df["account"] == input.account()] 15 | 16 | 17 | with ui.sidebar(): 18 | ui.input_select( 19 | "account", 20 | "Account", 21 | choices=[ 22 | "Berge & Berge", 23 | "Fritsch & Fritsch", 24 | "Hintz & Hintz", 25 | "Mosciski and Sons", 26 | "Wolff Ltd", 27 | ], 28 | ) 29 | 30 | with ui.layout_columns(): 31 | 32 | with ui.card(): 33 | ui.card_header("Model Metrics") 34 | 35 | @render_plotly 36 | def metric_plot(): 37 | # `account_data` is called similar to an input. 38 | if input.metric() == "ROC Curve": 39 | return plot_auc_curve( 40 | account_data(), "is_electronics", "training_score" 41 | ) 42 | else: 43 | return plot_precision_recall_curve( 44 | account_data(), "is_electronics", "training_score" 45 | ) 46 | 47 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 48 | 49 | with ui.card(): 50 | ui.card_header("Model Scores") 51 | 52 | @render_plotly 53 | def score_dist(): 54 | # `account_data` is called similar to an input. 55 | return plot_score_distribution(account_data()) 56 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.1-reactive-calc/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | with ui.sidebar(): 8 | ui.input_select( 9 | "account", 10 | "Account", 11 | choices=[ 12 | "Berge & Berge", 13 | "Fritsch & Fritsch", 14 | "Hintz & Hintz", 15 | "Mosciski and Sons", 16 | "Wolff Ltd", 17 | ], 18 | ) 19 | 20 | with ui.layout_columns(): 21 | 22 | with ui.card(): 23 | ui.card_header("Model Metrics") 24 | 25 | @render_plotly 26 | def metric_plot(): 27 | account_subset = df[df["account"] == input.account()] 28 | if input.metric() == "ROC Curve": 29 | return plot_auc_curve( 30 | account_subset, "is_electronics", "training_score" 31 | ) 32 | else: 33 | return plot_precision_recall_curve( 34 | account_subset, "is_electronics", "training_score" 35 | ) 36 | 37 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 38 | 39 | with ui.card(): 40 | ui.card_header("Model Scores") 41 | 42 | @render_plotly 43 | def score_dist(): 44 | account_subset = df[df["account"] == input.account()] 45 | return plot_score_distribution(account_subset) 46 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.1-reactive-calc/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.1-reactive-calc/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.1-reactive-calc/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.2-stacking-reactives/README: -------------------------------------------------------------------------------- 1 | We have a second sidebar input which allows the user to filter the dataset by the number of characters in the `text` field. 2 | Add a second reactive calculation to the app which filters the `account_data()` reactive. 3 | 4 | For reference `input.chars()` returns a tuple with the lower and upper range of a value, and you can filter the data frame with: 5 | `df[df["text"].str.len().between(*input.chars())]`. 6 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.2-stacking-reactives/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc 9 | def account_data(): 10 | return df[df["account"] == input.account()] 11 | 12 | 13 | # You can call reactive calculations from other reactive calculations. 14 | # This is very useful for performance because the calculation will be 15 | # minimally rerun. 16 | @reactive.calc() 17 | def character_filter(): 18 | return account_data()[account_data()["text"].str.len().between(*input.chars())] 19 | 20 | 21 | with ui.sidebar(): 22 | ui.input_select( 23 | "account", 24 | "Account", 25 | choices=[ 26 | "Berge & Berge", 27 | "Fritsch & Fritsch", 28 | "Hintz & Hintz", 29 | "Mosciski and Sons", 30 | "Wolff Ltd", 31 | ], 32 | ) 33 | 34 | ui.input_slider( 35 | "chars", 36 | "Text length", 37 | min=0, 38 | max=df["text"].str.len().max(), 39 | value=[500, 6000], 40 | ) 41 | 42 | with ui.layout_columns(): 43 | 44 | with ui.card(): 45 | ui.card_header("Model Metrics") 46 | 47 | @render_plotly 48 | def metric_plot(): 49 | if input.metric() == "ROC Curve": 50 | return plot_auc_curve( 51 | character_filter(), "is_electronics", "training_score" 52 | ) 53 | else: 54 | return plot_precision_recall_curve( 55 | character_filter(), "is_electronics", "training_score" 56 | ) 57 | 58 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 59 | 60 | with ui.card(): 61 | ui.card_header("Model Scores") 62 | 63 | @render_plotly 64 | def score_dist(): 65 | return plot_score_distribution(character_filter()) 66 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.2-stacking-reactives/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from data_import import df 3 | from shiny import reactive 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc 9 | def account_data(): 10 | return df[df["account"] == input.account()] 11 | 12 | 13 | with ui.sidebar(): 14 | ui.input_select( 15 | "account", 16 | "Account", 17 | choices=[ 18 | "Berge & Berge", 19 | "Fritsch & Fritsch", 20 | "Hintz & Hintz", 21 | "Mosciski and Sons", 22 | "Wolff Ltd", 23 | ], 24 | ) 25 | 26 | ui.input_slider( 27 | "chars", 28 | "Text length", 29 | min=0, 30 | max=df["text"].str.len().max(), 31 | value=[500, 6000], 32 | ) 33 | 34 | with ui.layout_columns(): 35 | 36 | with ui.card(): 37 | ui.card_header("Model Metrics") 38 | 39 | @render_plotly 40 | def metric_plot(): 41 | if input.metric() == "ROC Curve": 42 | return plot_auc_curve(account_data, "is_electronics", "training_score") 43 | else: 44 | return plot_precision_recall_curve( 45 | account_data(), "is_electronics", "training_score" 46 | ) 47 | 48 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 49 | 50 | with ui.card(): 51 | ui.card_header("Model Scores") 52 | 53 | @render_plotly 54 | def score_dist(): 55 | return plot_score_distribution(account_data()) 56 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.2-stacking-reactives/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.2-stacking-reactives/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/3.2-stacking-reactives/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/3-reactivity/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/4.1-render-express/README: -------------------------------------------------------------------------------- 1 | Use `render.express` to create a second dropdown which only contains the unique `sub_account` values of the filtered data. 2 | -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/4.1-render-express/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc() 9 | def account_data(): 10 | return df[ 11 | (df["account"] == input.account()) & (df["sub_account"] == input.sub_account()) 12 | ] 13 | 14 | 15 | @reactive.calc() 16 | def character_filter(): 17 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 18 | 19 | 20 | with ui.sidebar(): 21 | ui.input_select( 22 | "account", 23 | "Account", 24 | choices=[ 25 | "Berge & Berge", 26 | "Fritsch & Fritsch", 27 | "Hintz & Hintz", 28 | "Mosciski and Sons", 29 | "Wolff Ltd", 30 | ], 31 | ) 32 | 33 | # You shouldn't use return values with `render.express` instead just call the inputs 34 | # and rendering funcitons and they will be added to the app. 35 | @render.express 36 | def sub_selector(): 37 | # We can't use the account_data reactive here because we are using the 38 | # sub_account input as part of that reactive. 39 | choice_data = df[df["account"] == input.account()] 40 | choices = choice_data["sub_account"].unique().tolist() 41 | ui.input_select("sub_account", "Sub Account", choices=choices) 42 | 43 | ui.input_slider( 44 | "chars", 45 | "Text length", 46 | min=0, 47 | max=df["text"].str.len().max(), 48 | value=[500, 6000], 49 | ) 50 | 51 | with ui.layout_columns(): 52 | 53 | with ui.card(): 54 | ui.card_header("Model Metrics") 55 | 56 | @render_plotly 57 | def metric_plot(): 58 | if input.metric() == "ROC Curve": 59 | return plot_auc_curve( 60 | character_filter(), "is_electronics", "training_score" 61 | ) 62 | else: 63 | return plot_precision_recall_curve( 64 | character_filter(), "is_electronics", "training_score" 65 | ) 66 | 67 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 68 | 69 | with ui.card(): 70 | ui.card_header("Model Scores") 71 | 72 | @render_plotly 73 | def score_dist(): 74 | return plot_score_distribution(character_filter()) 75 | -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/4.1-render-express/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc 9 | def account_data(): 10 | return df[df["account"] == input.account()] 11 | 12 | 13 | @reactive.calc() 14 | def character_filter(): 15 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 16 | 17 | 18 | with ui.sidebar(): 19 | ui.input_select( 20 | "account", 21 | "Account", 22 | choices=[ 23 | "Berge & Berge", 24 | "Fritsch & Fritsch", 25 | "Hintz & Hintz", 26 | "Mosciski and Sons", 27 | "Wolff Ltd", 28 | ], 29 | ) 30 | 31 | ui.input_slider( 32 | "chars", 33 | "Text length", 34 | min=0, 35 | max=df["text"].str.len().max(), 36 | value=[500, 6000], 37 | ) 38 | 39 | with ui.layout_columns(): 40 | 41 | with ui.card(): 42 | ui.card_header("Model Metrics") 43 | 44 | @render_plotly 45 | def metric_plot(): 46 | if input.metric() == "ROC Curve": 47 | return plot_auc_curve( 48 | character_filter(), "is_electronics", "training_score" 49 | ) 50 | else: 51 | return plot_precision_recall_curve( 52 | character_filter(), "is_electronics", "training_score" 53 | ) 54 | 55 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 56 | 57 | with ui.card(): 58 | ui.card_header("Model Scores") 59 | 60 | @render_plotly 61 | def score_dist(): 62 | return plot_score_distribution(character_filter()) 63 | -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/4.1-render-express/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/4.1-render-express/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/4.1-render-express/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-dynamic-ui/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.1-tabs/README: -------------------------------------------------------------------------------- 1 | Use `ui.navset_tabs` and `ui.nav` to put the dataframe output on a second tab. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.1-tabs/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | 6 | infile = Path(__file__).parent / "weather.csv" 7 | weather = pd.read_csv(infile) 8 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 9 | 10 | 11 | app_ui = ui.page_fluid( 12 | ui.panel_title("Weather error"), 13 | ui.layout_sidebar( 14 | ui.panel_sidebar( 15 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 16 | ui.input_selectize( 17 | "cities", 18 | "Select Cities", 19 | weather["city"].unique().tolist(), 20 | selected="BUFFALO", 21 | multiple=True, 22 | ), 23 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 24 | width=3, 25 | ), 26 | ui.panel_main( 27 | ui.navset_tab( 28 | ui.nav( 29 | "Error", 30 | ui.output_plot("error_distribution"), 31 | ui.output_plot("error_by_day"), 32 | ), 33 | ui.nav("Data", ui.output_data_frame("data")), 34 | ) 35 | ), 36 | ), 37 | ) 38 | 39 | 40 | def server(input, output, session): 41 | @reactive.Calc 42 | def filtered_data(): 43 | df = weather.copy() 44 | df = df[df["city"].isin(input.cities())] 45 | df["date"] = pd.to_datetime(df["date"]) 46 | dates = pd.to_datetime(input.dates()) 47 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 48 | return df 49 | 50 | @output 51 | @render.plot 52 | def error_distribution(): 53 | return temp_distirbution(filtered_data()) 54 | 55 | @output 56 | @render.plot 57 | def error_by_day(): 58 | return daily_error(filtered_data(), input.alpha()) 59 | 60 | @output 61 | @render.data_frame 62 | def data(): 63 | return filtered_data() 64 | 65 | 66 | app = App(app_ui, server) 67 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.1-tabs/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | 12 | card1 = x.ui.card(ui.output_plot("Plot")) 13 | tab1 = ui.nav( 14 | "Tab1", 15 | card1, 16 | ui.output_text("some_text"), 17 | ) 18 | tab2 = ui.nav("Tab2", ui.output_data_frame("data")) 19 | tab3 = ui.nav("Tab3", ui.output_image("image")) 20 | ui.navset_tab(tab1, tab2, tab3) 21 | 22 | 23 | app_ui = ui.page_fluid( 24 | ui.layout_sidebar( 25 | ui.panel_sidebar( 26 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 27 | ui.input_selectize( 28 | "cities", 29 | "Select Cities", 30 | weather["city"].unique().tolist(), 31 | selected="BUFFALO", 32 | multiple=True, 33 | ), 34 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 35 | width=3, 36 | ), 37 | ui.panel_main( 38 | ui.output_plot("error_distribution"), 39 | ui.output_plot("error_by_day"), 40 | ui.output_data_frame("data"), 41 | ), 42 | ) 43 | ) 44 | 45 | 46 | def server(input, output, session): 47 | @reactive.Calc 48 | def filtered_data(): 49 | df = weather.copy() 50 | df = df[df["city"].isin(input.cities())] 51 | df["date"] = pd.to_datetime(df["date"]) 52 | dates = pd.to_datetime(input.dates()) 53 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 54 | return df 55 | 56 | @output 57 | @render.plot 58 | def error_distribution(): 59 | return temp_distirbution(filtered_data()) 60 | 61 | @output 62 | @render.plot 63 | def error_by_day(): 64 | return daily_error(filtered_data(), input.alpha()) 65 | 66 | @output 67 | @render.data_frame 68 | def data(): 69 | return filtered_data() 70 | 71 | 72 | app = App(app_ui, server) 73 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.1-tabs/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.1-tabs/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.2-cards/README: -------------------------------------------------------------------------------- 1 | Shiny has a few great components in the `experimental` module, try wrapping the two graphs in `x.ui.card()`. Note that you first need to import the experimental module with `import shiny.experimental as x`. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.2-cards/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | 12 | app_ui = ui.page_fluid( 13 | ui.panel_title("Weather error"), 14 | ui.layout_sidebar( 15 | ui.panel_sidebar( 16 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 17 | ui.input_selectize( 18 | "cities", 19 | "Select Cities", 20 | weather["city"].unique().tolist(), 21 | selected="BUFFALO", 22 | multiple=True, 23 | ), 24 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 25 | width=3, 26 | ), 27 | ui.panel_main( 28 | ui.navset_tab( 29 | ui.nav( 30 | "Error", 31 | x.ui.card( 32 | x.ui.card_header("Distribution"), 33 | ui.output_plot("error_distribution"), 34 | ), 35 | x.ui.card( 36 | x.ui.card_header("Error by day"), 37 | ui.output_plot("error_by_day"), 38 | ), 39 | ), 40 | ui.nav("Data", ui.output_data_frame("data")), 41 | ) 42 | ), 43 | ), 44 | ) 45 | 46 | 47 | def server(input, output, session): 48 | @reactive.Calc 49 | def filtered_data(): 50 | df = weather.copy() 51 | df = df[df["city"].isin(input.cities())] 52 | df["date"] = pd.to_datetime(df["date"]) 53 | dates = pd.to_datetime(input.dates()) 54 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 55 | return df 56 | 57 | @output 58 | @render.plot 59 | def error_distribution(): 60 | return temp_distirbution(filtered_data()) 61 | 62 | @output 63 | @render.plot 64 | def error_by_day(): 65 | return daily_error(filtered_data(), input.alpha()) 66 | 67 | @output 68 | @render.data_frame 69 | def data(): 70 | return filtered_data() 71 | 72 | 73 | app = App(app_ui, server) 74 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.2-cards/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | 12 | app_ui = ui.page_fluid( 13 | ui.panel_title("Weather error"), 14 | ui.layout_sidebar( 15 | ui.panel_sidebar( 16 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 17 | ui.input_selectize( 18 | "cities", 19 | "Select Cities", 20 | weather["city"].unique().tolist(), 21 | selected="BUFFALO", 22 | multiple=True, 23 | ), 24 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 25 | width=3, 26 | ), 27 | ui.panel_main( 28 | ui.navset_tab( 29 | ui.nav( 30 | "Error", 31 | ui.output_plot("error_distribution"), 32 | ui.output_plot("error_by_day"), 33 | ), 34 | ui.nav("Data", ui.output_data_frame("data")), 35 | ) 36 | ), 37 | ), 38 | ) 39 | 40 | 41 | def server(input, output, session): 42 | @reactive.Calc 43 | def filtered_data(): 44 | df = weather.copy() 45 | df = df[df["city"].isin(input.cities())] 46 | df["date"] = pd.to_datetime(df["date"]) 47 | dates = pd.to_datetime(input.dates()) 48 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 49 | return df 50 | 51 | @output 52 | @render.plot 53 | def error_distribution(): 54 | return temp_distirbution(filtered_data()) 55 | 56 | @output 57 | @render.plot 58 | def error_by_day(): 59 | return daily_error(filtered_data(), input.alpha()) 60 | 61 | @output 62 | @render.data_frame 63 | def data(): 64 | return filtered_data() 65 | 66 | 67 | app = App(app_ui, server) 68 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.2-cards/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.2-cards/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.3-layout/README: -------------------------------------------------------------------------------- 1 | Use `ui.row` and `ui.col` to layout the two cards side-by-side. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.3-layout/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | 12 | app_ui = ui.page_fluid( 13 | ui.panel_title("Weather error"), 14 | ui.layout_sidebar( 15 | ui.panel_sidebar( 16 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 17 | ui.input_selectize( 18 | "cities", 19 | "Select Cities", 20 | weather["city"].unique().tolist(), 21 | selected="BUFFALO", 22 | multiple=True, 23 | ), 24 | width=3, 25 | ), 26 | ui.panel_main( 27 | ui.navset_tab( 28 | ui.nav( 29 | "Error", 30 | ui.row( 31 | ui.column( 32 | 6, 33 | x.ui.card( 34 | x.ui.card_header("Distribution"), 35 | ui.output_plot("error_distribution"), 36 | ), 37 | ), 38 | ui.column( 39 | 6, 40 | x.ui.card( 41 | x.ui.card_header("Error by day"), 42 | ui.output_plot("error_by_day"), 43 | ui.input_slider( 44 | "alpha", "Plot Alpha", value=0.5, min=0, max=1 45 | ), 46 | ), 47 | ), 48 | ), 49 | ), 50 | ui.nav("Data", ui.output_data_frame("data")), 51 | ) 52 | ), 53 | ), 54 | ) 55 | 56 | 57 | def server(input, output, session): 58 | @reactive.Calc 59 | def filtered_data(): 60 | df = weather.copy() 61 | df = df[df["city"].isin(input.cities())] 62 | df["date"] = pd.to_datetime(df["date"]) 63 | dates = pd.to_datetime(input.dates()) 64 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 65 | return df 66 | 67 | @output 68 | @render.plot 69 | def error_distribution(): 70 | return temp_distirbution(filtered_data()) 71 | 72 | @output 73 | @render.plot 74 | def error_by_day(): 75 | return daily_error(filtered_data(), input.alpha()) 76 | 77 | @output 78 | @render.data_frame 79 | def data(): 80 | return filtered_data() 81 | 82 | 83 | app = App(app_ui, server) 84 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.3-layout/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | 12 | app_ui = ui.page_fluid( 13 | ui.panel_title("Weather error"), 14 | ui.layout_sidebar( 15 | ui.panel_sidebar( 16 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 17 | ui.input_selectize( 18 | "cities", 19 | "Select Cities", 20 | weather["city"].unique().tolist(), 21 | selected="BUFFALO", 22 | multiple=True, 23 | ), 24 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 25 | width=3, 26 | ), 27 | ui.panel_main( 28 | ui.navset_tab( 29 | ui.nav( 30 | "Error", 31 | x.ui.card( 32 | x.ui.card_header("Distribution"), 33 | ui.output_plot("error_distribution"), 34 | ), 35 | x.ui.card( 36 | x.ui.card_header("Error by day"), 37 | ui.output_plot("error_by_day"), 38 | ), 39 | ), 40 | ui.nav("Data", ui.output_data_frame("data")), 41 | ) 42 | ), 43 | ), 44 | ) 45 | 46 | 47 | def server(input, output, session): 48 | @reactive.Calc 49 | def filtered_data(): 50 | df = weather.copy() 51 | df = df[df["city"].isin(input.cities())] 52 | df["date"] = pd.to_datetime(df["date"]) 53 | dates = pd.to_datetime(input.dates()) 54 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 55 | return df 56 | 57 | @output 58 | @render.plot 59 | def error_distribution(): 60 | return temp_distirbution(filtered_data()) 61 | 62 | @output 63 | @render.plot 64 | def error_by_day(): 65 | return daily_error(filtered_data(), input.alpha()) 66 | 67 | @output 68 | @render.data_frame 69 | def data(): 70 | return filtered_data() 71 | 72 | 73 | app = App(app_ui, server) 74 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.3-layout/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.3-layout/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.4-ui-composition/README: -------------------------------------------------------------------------------- 1 | Our ui code is getting a little complicated. Try extracting the two tab sections into their own objects. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.4-ui-composition/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column( 16 | 6, 17 | x.ui.card( 18 | x.ui.card_header("Distribution"), 19 | ui.output_plot("error_distribution"), 20 | ), 21 | ), 22 | ui.column( 23 | 6, 24 | x.ui.card( 25 | x.ui.card_header("Error by day"), 26 | ui.output_plot("error_by_day"), 27 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 28 | ), 29 | ), 30 | ), 31 | ) 32 | 33 | app_ui = ui.page_fluid( 34 | ui.panel_title("Weather error"), 35 | ui.layout_sidebar( 36 | ui.panel_sidebar( 37 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 38 | ui.input_selectize( 39 | "cities", 40 | "Select Cities", 41 | weather["city"].unique().tolist(), 42 | selected="BUFFALO", 43 | multiple=True, 44 | ), 45 | width=3, 46 | ), 47 | ui.panel_main( 48 | ui.navset_tab( 49 | error_tab, 50 | data_tab, 51 | ) 52 | ), 53 | ), 54 | ) 55 | 56 | 57 | def server(input, output, session): 58 | @reactive.Calc 59 | def filtered_data(): 60 | df = weather.copy() 61 | df = df[df["city"].isin(input.cities())] 62 | df["date"] = pd.to_datetime(df["date"]) 63 | dates = pd.to_datetime(input.dates()) 64 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 65 | return df 66 | 67 | @output 68 | @render.plot 69 | def error_distribution(): 70 | return temp_distirbution(filtered_data()) 71 | 72 | @output 73 | @render.plot 74 | def error_by_day(): 75 | return daily_error(filtered_data(), input.alpha()) 76 | 77 | @output 78 | @render.data_frame 79 | def data(): 80 | return filtered_data() 81 | 82 | 83 | app = App(app_ui, server) 84 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.4-ui-composition/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | 12 | app_ui = ui.page_fluid( 13 | ui.panel_title("Weather error"), 14 | ui.layout_sidebar( 15 | ui.panel_sidebar( 16 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 17 | ui.input_selectize( 18 | "cities", 19 | "Select Cities", 20 | weather["city"].unique().tolist(), 21 | selected="BUFFALO", 22 | multiple=True, 23 | ), 24 | width=3, 25 | ), 26 | ui.panel_main( 27 | ui.navset_tab( 28 | ui.nav( 29 | "Error", 30 | ui.row( 31 | ui.column( 32 | 6, 33 | x.ui.card( 34 | x.ui.card_header("Distribution"), 35 | ui.output_plot("error_distribution"), 36 | ), 37 | ), 38 | ui.column( 39 | 6, 40 | x.ui.card( 41 | x.ui.card_header("Error by day"), 42 | ui.output_plot("error_by_day"), 43 | ui.input_slider( 44 | "alpha", "Plot Alpha", value=0.5, min=0, max=1 45 | ), 46 | ), 47 | ), 48 | ), 49 | ), 50 | ui.nav("Data", ui.output_data_frame("data")), 51 | ) 52 | ), 53 | ), 54 | ) 55 | 56 | 57 | def server(input, output, session): 58 | @reactive.Calc 59 | def filtered_data(): 60 | df = weather.copy() 61 | df = df[df["city"].isin(input.cities())] 62 | df["date"] = pd.to_datetime(df["date"]) 63 | dates = pd.to_datetime(input.dates()) 64 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 65 | return df 66 | 67 | @output 68 | @render.plot 69 | def error_distribution(): 70 | return temp_distirbution(filtered_data()) 71 | 72 | @output 73 | @render.plot 74 | def error_by_day(): 75 | return daily_error(filtered_data(), input.alpha()) 76 | 77 | @output 78 | @render.data_frame 79 | def data(): 80 | return filtered_data() 81 | 82 | 83 | app = App(app_ui, server) 84 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.4-ui-composition/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.4-ui-composition/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.5-value-boxes/README: -------------------------------------------------------------------------------- 1 | Use the `x.value_box` container function to add three value boxes on their own `ui.row`. Note that we have provided the server functions for you. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.5-value-boxes/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column(4, x.ui.value_box("Hot days", ui.output_text("hot_days"))), 16 | ui.column(4, x.ui.value_box("Cold days", ui.output_text("cold_days"))), 17 | ui.column(4, x.ui.value_box("Mean Error", ui.output_text("mean_error"))), 18 | ), 19 | ui.row( 20 | ui.column( 21 | 6, 22 | x.ui.card( 23 | x.ui.card_header("Distribution"), 24 | ui.output_plot("error_distribution"), 25 | ), 26 | ), 27 | ui.column( 28 | 6, 29 | x.ui.card( 30 | x.ui.card_header("Error by day"), 31 | ui.output_plot("error_by_day"), 32 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 33 | ), 34 | ), 35 | ), 36 | ) 37 | 38 | app_ui = ui.page_fluid( 39 | ui.panel_title("Weather error"), 40 | ui.layout_sidebar( 41 | ui.panel_sidebar( 42 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 43 | ui.input_selectize( 44 | "cities", 45 | "Select Cities", 46 | weather["city"].unique().tolist(), 47 | selected="BUFFALO", 48 | multiple=True, 49 | ), 50 | width=3, 51 | ), 52 | ui.panel_main( 53 | ui.navset_tab( 54 | error_tab, 55 | data_tab, 56 | ) 57 | ), 58 | ), 59 | ) 60 | 61 | 62 | def server(input, output, session): 63 | @reactive.Calc 64 | def filtered_data() -> pd.DataFrame: 65 | df = weather.copy() 66 | df = df[df["city"].isin(input.cities())] 67 | df["date"] = pd.to_datetime(df["date"]) 68 | dates = pd.to_datetime(input.dates()) 69 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 70 | return df 71 | 72 | @output 73 | @render.plot 74 | def error_distribution(): 75 | return temp_distirbution(filtered_data()) 76 | 77 | @output 78 | @render.plot 79 | def error_by_day(): 80 | return daily_error(filtered_data(), input.alpha()) 81 | 82 | @output 83 | @render.data_frame 84 | def data(): 85 | return filtered_data() 86 | 87 | @output 88 | @render.text 89 | def mean_error(): 90 | mean_error = filtered_data()["error"].mean() 91 | return round(mean_error, 2) 92 | 93 | @output 94 | @render.text 95 | def hot_days(): 96 | hot_days = filtered_data()["error"] > 0 97 | return sum(hot_days) 98 | 99 | @output 100 | @render.text 101 | def cold_days(): 102 | hot_days = filtered_data()["error"] < 0 103 | return sum(hot_days) 104 | 105 | 106 | app = App(app_ui, server) 107 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.5-value-boxes/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column( 16 | 6, 17 | x.ui.card( 18 | x.ui.card_header("Distribution"), 19 | ui.output_plot("error_distribution"), 20 | ), 21 | ), 22 | ui.column( 23 | 6, 24 | x.ui.card( 25 | x.ui.card_header("Error by day"), 26 | ui.output_plot("error_by_day"), 27 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 28 | ), 29 | ), 30 | ), 31 | ) 32 | 33 | app_ui = ui.page_fluid( 34 | ui.panel_title("Weather error"), 35 | ui.layout_sidebar( 36 | ui.panel_sidebar( 37 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 38 | ui.input_selectize( 39 | "cities", 40 | "Select Cities", 41 | weather["city"].unique().tolist(), 42 | selected="BUFFALO", 43 | multiple=True, 44 | ), 45 | width=3, 46 | ), 47 | ui.panel_main( 48 | ui.navset_tab( 49 | error_tab, 50 | data_tab, 51 | ) 52 | ), 53 | ), 54 | ) 55 | 56 | 57 | def server(input, output, session): 58 | @reactive.Calc 59 | def filtered_data(): 60 | df = weather.copy() 61 | df = df[df["city"].isin(input.cities())] 62 | df["date"] = pd.to_datetime(df["date"]) 63 | dates = pd.to_datetime(input.dates()) 64 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 65 | return df 66 | 67 | @output 68 | @render.plot 69 | def error_distribution(): 70 | return temp_distirbution(filtered_data()) 71 | 72 | @output 73 | @render.plot 74 | def error_by_day(): 75 | return daily_error(filtered_data(), input.alpha()) 76 | 77 | @output 78 | @render.data_frame 79 | def data(): 80 | return filtered_data() 81 | 82 | @output 83 | @render.text 84 | def mean_error(): 85 | mean_error = filtered_data()["error"].mean() 86 | return round(mean_error, 2) 87 | 88 | @output 89 | @render.text 90 | def hot_days(): 91 | hot_days = filtered_data()["error"] > 0 92 | return sum(hot_days) 93 | 94 | @output 95 | @render.text 96 | def cold_days(): 97 | hot_days = filtered_data()["error"] < 0 98 | return sum(hot_days) 99 | 100 | 101 | app = App(app_ui, server) 102 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.5-value-boxes/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.5-value-boxes/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.6-dynamic-ui/README: -------------------------------------------------------------------------------- 1 | We've added a `state` selector, use `ui.output_ui` to dynamically generate a city selector which only includes cities from the selected states. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.6-dynamic-ui/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column( 16 | 4, x.ui.value_box("Hotter than forecast", ui.output_text("hot_days")) 17 | ), 18 | ui.column( 19 | 4, x.ui.value_box("Colder than forecast", ui.output_text("cold_days")) 20 | ), 21 | ui.column(4, x.ui.value_box("Mean Error", ui.output_text("mean_error"))), 22 | ), 23 | ui.row( 24 | ui.column( 25 | 6, 26 | x.ui.card( 27 | x.ui.card_header("Distribution"), 28 | ui.output_plot("error_distribution"), 29 | ), 30 | ), 31 | ui.column( 32 | 6, 33 | x.ui.card( 34 | x.ui.card_header("Error by day"), 35 | ui.output_plot("error_by_day"), 36 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 37 | ), 38 | ), 39 | ), 40 | ) 41 | 42 | app_ui = ui.page_fluid( 43 | ui.panel_title("Weather error"), 44 | ui.layout_sidebar( 45 | ui.panel_sidebar( 46 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 47 | ui.input_selectize( 48 | "states", 49 | "Select States", 50 | weather["state"].unique().tolist(), 51 | selected="CO", 52 | multiple=True, 53 | ), 54 | ui.output_ui("cities_ui"), 55 | width=3, 56 | ), 57 | ui.panel_main( 58 | ui.navset_tab( 59 | error_tab, 60 | data_tab, 61 | ) 62 | ), 63 | ), 64 | ) 65 | 66 | 67 | def server(input, output, session): 68 | @output 69 | @render.ui 70 | def cities_ui(): 71 | df = weather.copy() 72 | df = df[df["state"].isin(input.states())] 73 | city_options = df["city"].unique().tolist() 74 | return ui.input_selectize( 75 | "cities", 76 | "Select Cities", 77 | choices=city_options, 78 | selected=city_options[0], 79 | multiple=True, 80 | ) 81 | 82 | @reactive.Calc 83 | def filtered_data() -> pd.DataFrame: 84 | df = weather.copy() 85 | df = df[df["city"].isin(input.cities())] 86 | df["date"] = pd.to_datetime(df["date"]) 87 | dates = pd.to_datetime(input.dates()) 88 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 89 | return df 90 | 91 | @output 92 | @render.plot 93 | def error_distribution(): 94 | return temp_distirbution(filtered_data()) 95 | 96 | @output 97 | @render.plot 98 | def error_by_day(): 99 | return daily_error(filtered_data(), input.alpha()) 100 | 101 | @output 102 | @render.data_frame 103 | def data(): 104 | return filtered_data() 105 | 106 | @output 107 | @render.text 108 | def mean_error(): 109 | mean_error = filtered_data()["error"].mean() 110 | return round(mean_error, 2) 111 | 112 | @output 113 | @render.text 114 | def hot_days(): 115 | hot_days = filtered_data()["error"] > 0 116 | return sum(hot_days) 117 | 118 | @output 119 | @render.text 120 | def cold_days(): 121 | hot_days = filtered_data()["error"] < 0 122 | return sum(hot_days) 123 | 124 | 125 | app = App(app_ui, server) 126 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.6-dynamic-ui/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column( 16 | 4, x.ui.value_box("Hotter than forecast", ui.output_text("hot_days")) 17 | ), 18 | ui.column( 19 | 4, x.ui.value_box("Colder than forecast", ui.output_text("cold_days")) 20 | ), 21 | ui.column(4, x.ui.value_box("Mean Error", ui.output_text("mean_error"))), 22 | ), 23 | ui.row( 24 | ui.column( 25 | 6, 26 | x.ui.card( 27 | x.ui.card_header("Distribution"), 28 | ui.output_plot("error_distribution"), 29 | ), 30 | ), 31 | ui.column( 32 | 6, 33 | x.ui.card( 34 | x.ui.card_header("Error by day"), 35 | ui.output_plot("error_by_day"), 36 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 37 | ), 38 | ), 39 | ), 40 | ) 41 | 42 | app_ui = ui.page_fluid( 43 | ui.panel_title("Weather error"), 44 | ui.layout_sidebar( 45 | ui.panel_sidebar( 46 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 47 | ui.input_selectize( 48 | "states", 49 | "Select States", 50 | weather["state"].unique().tolist(), 51 | multiple=True, 52 | ), 53 | width=3, 54 | ), 55 | ui.panel_main( 56 | ui.navset_tab( 57 | error_tab, 58 | data_tab, 59 | ) 60 | ), 61 | ), 62 | ) 63 | 64 | 65 | def server(input, output, session): 66 | @reactive.Calc 67 | def filtered_data() -> pd.DataFrame: 68 | df = weather.copy() 69 | df = df[df["city"].isin(input.cities())] 70 | df["date"] = pd.to_datetime(df["date"]) 71 | dates = pd.to_datetime(input.dates()) 72 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 73 | return df 74 | 75 | @output 76 | @render.plot 77 | def error_distribution(): 78 | return temp_distirbution(filtered_data()) 79 | 80 | @output 81 | @render.plot 82 | def error_by_day(): 83 | return daily_error(filtered_data(), input.alpha()) 84 | 85 | @output 86 | @render.data_frame 87 | def data(): 88 | return filtered_data() 89 | 90 | @output 91 | @render.text 92 | def mean_error(): 93 | mean_error = filtered_data()["error"].mean() 94 | return round(mean_error, 2) 95 | 96 | @output 97 | @render.text 98 | def hot_days(): 99 | hot_days = filtered_data()["error"] > 0 100 | return sum(hot_days) 101 | 102 | @output 103 | @render.text 104 | def cold_days(): 105 | hot_days = filtered_data()["error"] < 0 106 | return sum(hot_days) 107 | 108 | 109 | app = App(app_ui, server) 110 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.6-dynamic-ui/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.6-dynamic-ui/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.7-conditional-panel/README: -------------------------------------------------------------------------------- 1 | Use `ui.conditional_panel` to show or hide a column selector based on whether the "Data" tab is visible. Note that you'll need to use a JavaScript logical test for conditional panel which should be `input.tabs === 'Data'`. -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.7-conditional-panel/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column( 16 | 4, x.ui.value_box("Hotter than forecast", ui.output_text("hot_days")) 17 | ), 18 | ui.column( 19 | 4, x.ui.value_box("Colder than forecast", ui.output_text("cold_days")) 20 | ), 21 | ui.column(4, x.ui.value_box("Mean Error", ui.output_text("mean_error"))), 22 | ), 23 | ui.row( 24 | ui.column( 25 | 6, 26 | x.ui.card( 27 | x.ui.card_header("Distribution"), 28 | ui.output_plot("error_distribution"), 29 | ), 30 | ), 31 | ui.column( 32 | 6, 33 | x.ui.card( 34 | x.ui.card_header("Error by day"), 35 | ui.output_plot("error_by_day"), 36 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 37 | ), 38 | ), 39 | ), 40 | ) 41 | 42 | app_ui = ui.page_fluid( 43 | ui.panel_title("Weather error"), 44 | ui.layout_sidebar( 45 | ui.panel_sidebar( 46 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 47 | ui.input_selectize( 48 | "states", 49 | "Select States", 50 | weather["state"].unique().tolist(), 51 | selected="CO", 52 | multiple=True, 53 | ), 54 | ui.output_ui("cities_ui"), 55 | ui.panel_conditional( 56 | "input.tabs === 'Data'", 57 | ui.input_selectize( 58 | "columns", 59 | "Display Columns", 60 | choices=weather.columns.tolist(), 61 | selected=weather.columns.tolist(), 62 | multiple=True, 63 | ), 64 | ), 65 | width=3, 66 | ), 67 | ui.panel_main(ui.navset_tab(error_tab, data_tab, id="tabs")), 68 | ), 69 | ) 70 | 71 | 72 | def server(input, output, session): 73 | @output 74 | @render.ui 75 | def cities_ui(): 76 | df = weather.copy() 77 | df = df[df["state"].isin(input.states())] 78 | city_options = df["city"].unique().tolist() 79 | return ui.input_selectize( 80 | "cities", 81 | "Select Cities", 82 | choices=city_options, 83 | selected=city_options[0], 84 | multiple=True, 85 | ) 86 | 87 | @reactive.Calc 88 | def filtered_data() -> pd.DataFrame: 89 | df = weather.copy() 90 | df = df[df["city"].isin(input.cities())] 91 | df["date"] = pd.to_datetime(df["date"]) 92 | dates = pd.to_datetime(input.dates()) 93 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 94 | return df 95 | 96 | @output 97 | @render.plot 98 | def error_distribution(): 99 | return temp_distirbution(filtered_data()) 100 | 101 | @output 102 | @render.plot 103 | def error_by_day(): 104 | return daily_error(filtered_data(), input.alpha()) 105 | 106 | @output 107 | @render.data_frame 108 | def data(): 109 | return filtered_data().loc[:, input.columns()] 110 | 111 | @output 112 | @render.text 113 | def mean_error(): 114 | mean_error = filtered_data()["error"].mean() 115 | return round(mean_error, 2) 116 | 117 | @output 118 | @render.text 119 | def hot_days(): 120 | hot_days = filtered_data()["error"] > 0 121 | return sum(hot_days) 122 | 123 | @output 124 | @render.text 125 | def cold_days(): 126 | hot_days = filtered_data()["error"] < 0 127 | return sum(hot_days) 128 | 129 | 130 | app = App(app_ui, server) 131 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.7-conditional-panel/app.py: -------------------------------------------------------------------------------- 1 | from shiny import ui, render, reactive, App 2 | import pandas as pd 3 | from pathlib import Path 4 | from plots import temp_distirbution, daily_error 5 | import shiny.experimental as x 6 | 7 | infile = Path(__file__).parent / "weather.csv" 8 | weather = pd.read_csv(infile) 9 | weather["error"] = weather["observed_temp"] - weather["forecast_temp"] 10 | 11 | data_tab = ui.nav("Data", ui.output_data_frame("data")) 12 | error_tab = ui.nav( 13 | "Error", 14 | ui.row( 15 | ui.column( 16 | 4, x.ui.value_box("Hotter than forecast", ui.output_text("hot_days")) 17 | ), 18 | ui.column( 19 | 4, x.ui.value_box("Colder than forecast", ui.output_text("cold_days")) 20 | ), 21 | ui.column(4, x.ui.value_box("Mean Error", ui.output_text("mean_error"))), 22 | ), 23 | ui.row( 24 | ui.column( 25 | 6, 26 | x.ui.card( 27 | x.ui.card_header("Distribution"), 28 | ui.output_plot("error_distribution"), 29 | ), 30 | ), 31 | ui.column( 32 | 6, 33 | x.ui.card( 34 | x.ui.card_header("Error by day"), 35 | ui.output_plot("error_by_day"), 36 | ui.input_slider("alpha", "Plot Alpha", value=0.5, min=0, max=1), 37 | ), 38 | ), 39 | ), 40 | ) 41 | 42 | app_ui = ui.page_fluid( 43 | ui.panel_title("Weather error"), 44 | ui.layout_sidebar( 45 | ui.panel_sidebar( 46 | ui.input_date_range("dates", "Date", start="2022-01-01", end="2022-01-30"), 47 | ui.input_selectize( 48 | "states", 49 | "Select States", 50 | weather["state"].unique().tolist(), 51 | selected="CO", 52 | multiple=True, 53 | ), 54 | ui.output_ui("cities_ui"), 55 | width=3, 56 | ), 57 | ui.panel_main( 58 | ui.navset_tab( 59 | error_tab, 60 | data_tab, 61 | ) 62 | ), 63 | ), 64 | ) 65 | 66 | 67 | def server(input, output, session): 68 | @output 69 | @render.ui 70 | def cities_ui(): 71 | df = weather.copy() 72 | df = df[df["state"].isin(input.states())] 73 | city_options = df["city"].unique().tolist() 74 | return ui.input_selectize( 75 | "cities", 76 | "Select Cities", 77 | choices=city_options, 78 | selected=city_options[0], 79 | multiple=True, 80 | ) 81 | 82 | @reactive.Calc 83 | def filtered_data() -> pd.DataFrame: 84 | df = weather.copy() 85 | df = df[df["city"].isin(input.cities())] 86 | df["date"] = pd.to_datetime(df["date"]) 87 | dates = pd.to_datetime(input.dates()) 88 | df = df[(df["date"] > dates[0]) & (df["date"] <= dates[1])] 89 | return df 90 | 91 | @output 92 | @render.plot 93 | def error_distribution(): 94 | return temp_distirbution(filtered_data()) 95 | 96 | @output 97 | @render.plot 98 | def error_by_day(): 99 | return daily_error(filtered_data(), input.alpha()) 100 | 101 | @output 102 | @render.data_frame 103 | def data(): 104 | return filtered_data() 105 | 106 | @output 107 | @render.text 108 | def mean_error(): 109 | mean_error = filtered_data()["error"].mean() 110 | return round(mean_error, 2) 111 | 112 | @output 113 | @render.text 114 | def hot_days(): 115 | hot_days = filtered_data()["error"] > 0 116 | return sum(hot_days) 117 | 118 | @output 119 | @render.text 120 | def cold_days(): 121 | hot_days = filtered_data()["error"] < 0 122 | return sum(hot_days) 123 | 124 | 125 | app = App(app_ui, server) 126 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.7-conditional-panel/plots.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from plotnine import ( 3 | ggplot, 4 | aes, 5 | geom_density, 6 | theme_light, 7 | labs, 8 | geom_point, 9 | theme, 10 | element_text, 11 | ) 12 | 13 | 14 | def temp_distirbution(plot_df: pd.DataFrame) -> ggplot: 15 | plot_df = plot_df[["observed_temp", "forecast_temp", "date"]] 16 | plot_df = pd.melt( 17 | plot_df, id_vars="date", value_vars=["observed_temp", "forecast_temp"] 18 | ) 19 | out = ( 20 | ggplot(plot_df, aes(x="value", group="variable", color="variable")) 21 | + geom_density() 22 | + theme_light() 23 | ) 24 | return out 25 | 26 | 27 | def daily_error(plot_df: pd.DataFrame, alpha: float) -> ggplot: 28 | out = ( 29 | ggplot(plot_df, aes(x="date", y="error")) 30 | + geom_point(alpha=alpha) 31 | + theme_light() 32 | + theme(axis_text_x=element_text(rotation=45, hjust=1)) 33 | ) 34 | return out 35 | -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/4.7-conditional-panel/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/4-ui-customization/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.1-reactive-event/README: -------------------------------------------------------------------------------- 1 | Add a `ui.input_action_button` and a `@reactive.event` decorator to make the app only update when the button is clicked. 2 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.1-reactive-event/app-solution.py: -------------------------------------------------------------------------------- 1 | from shutil import ignore_patterns 2 | from shiny.express import render, ui, input 3 | from shiny import reactive 4 | from data_import import df 5 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 6 | from shinywidgets import render_plotly 7 | 8 | 9 | @reactive.calc() 10 | def account_data(): 11 | return df[ 12 | (df["account"] == input.account()) & (df["sub_account"] == input.sub_account()) 13 | ] 14 | 15 | 16 | # You can control execution by adding `@reactive.event` to any reactive function. 17 | # This tells Shiny to only trigger this renderer when a particular input changes. 18 | # Note that when inputs are used in reactive.events they are not called (input.update not input.update()) 19 | # This is because we are watching the reactive itself for changes, not making use of the current value. 20 | @reactive.calc() 21 | @reactive.event(input.update, ignore_init=True) 22 | def character_filter(): 23 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 24 | 25 | 26 | with ui.sidebar(): 27 | ui.input_select( 28 | "account", 29 | "Account", 30 | choices=[ 31 | "Berge & Berge", 32 | "Fritsch & Fritsch", 33 | "Hintz & Hintz", 34 | "Mosciski and Sons", 35 | "Wolff Ltd", 36 | ], 37 | ) 38 | 39 | @render.express 40 | def sub_selector(): 41 | # We can't use the account_data reactive here because we are using the 42 | # sub_account input as part of that reactive. 43 | choice_data = df[df["account"] == input.account()] 44 | choices = choice_data["sub_account"].unique().tolist() 45 | ui.input_select("sub_account", "Sub Account", choices=choices) 46 | 47 | ui.input_slider( 48 | "chars", 49 | "Text length", 50 | min=0, 51 | max=df["text"].str.len().max(), 52 | value=[500, 6000], 53 | ) 54 | 55 | ui.input_action_button("update", "Update", class_="btn-primary") 56 | 57 | with ui.layout_columns(): 58 | 59 | with ui.card(): 60 | ui.card_header("Model Metrics") 61 | 62 | @render_plotly 63 | def metric_plot(): 64 | if input.metric() == "ROC Curve": 65 | return plot_auc_curve( 66 | character_filter(), "is_electronics", "training_score" 67 | ) 68 | else: 69 | return plot_precision_recall_curve( 70 | character_filter(), "is_electronics", "training_score" 71 | ) 72 | 73 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 74 | 75 | with ui.card(): 76 | ui.card_header("Model Scores") 77 | 78 | @render_plotly 79 | def score_dist(): 80 | return plot_score_distribution(character_filter()) 81 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.1-reactive-event/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc() 9 | def account_data(): 10 | return df[ 11 | (df["account"] == input.account()) & (df["sub_account"] == input.sub_account()) 12 | ] 13 | 14 | 15 | @reactive.calc() 16 | def character_filter(): 17 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 18 | 19 | 20 | with ui.sidebar(): 21 | ui.input_select( 22 | "account", 23 | "Account", 24 | choices=[ 25 | "Berge & Berge", 26 | "Fritsch & Fritsch", 27 | "Hintz & Hintz", 28 | "Mosciski and Sons", 29 | "Wolff Ltd", 30 | ], 31 | ) 32 | 33 | @render.express 34 | def sub_selector(): 35 | # We can't use the account_data reactive here because we are using the 36 | # sub_account input as part of that reactive. 37 | choice_data = df[df["account"] == input.account()] 38 | choices = choice_data["sub_account"].unique().tolist() 39 | ui.input_select("sub_account", "Sub Account", choices=choices) 40 | 41 | ui.input_slider( 42 | "chars", 43 | "Text length", 44 | min=0, 45 | max=df["text"].str.len().max(), 46 | value=[500, 6000], 47 | ) 48 | 49 | with ui.layout_columns(): 50 | 51 | with ui.card(): 52 | ui.card_header("Model Metrics") 53 | 54 | def metric_plot(): 55 | if input.metric() == "ROC Curve": 56 | return plot_auc_curve( 57 | character_filter(), "is_electronics", "training_score" 58 | ) 59 | else: 60 | return plot_precision_recall_curve( 61 | character_filter(), "is_electronics", "training_score" 62 | ) 63 | 64 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 65 | 66 | with ui.card(): 67 | ui.card_header("Model Scores") 68 | 69 | @render_plotly 70 | def score_dist(): 71 | return plot_score_distribution(character_filter()) 72 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.1-reactive-event/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.1-reactive-event/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.1-reactive-event/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.2-reactive-effect/README: -------------------------------------------------------------------------------- 1 | Add a `ui.input_action_button` and a `@reactive.event` decorator to make a button that, when clicked, resets the slider to its initial values. 2 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.2-reactive-effect/app-solution.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc() 9 | def account_data(): 10 | return df[ 11 | (df["account"] == input.account()) & (df["sub_account"] == input.sub_account()) 12 | ] 13 | 14 | 15 | @reactive.calc() 16 | def character_filter(): 17 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 18 | 19 | 20 | # Reactive effects are used for side effects, not values. 21 | # They are similar to callback functions in other frameworks. 22 | @reactive.effect 23 | @reactive.event(input.reset) 24 | def reset_slider(): 25 | ui.update_slider("chars", value=[500, 6000]) 26 | 27 | 28 | with ui.sidebar(): 29 | ui.input_select( 30 | "account", 31 | "Account", 32 | choices=[ 33 | "Berge & Berge", 34 | "Fritsch & Fritsch", 35 | "Hintz & Hintz", 36 | "Mosciski and Sons", 37 | "Wolff Ltd", 38 | ], 39 | ) 40 | 41 | @render.express 42 | def sub_selector(): 43 | # We can't use the account_data reactive here because we are using the 44 | # sub_account input as part of that reactive. 45 | choice_data = df[df["account"] == input.account()] 46 | choices = choice_data["sub_account"].unique().tolist() 47 | ui.input_select("sub_account", "Sub Account", choices=choices) 48 | 49 | ui.input_slider( 50 | "chars", 51 | "Text length", 52 | min=0, 53 | max=df["text"].str.len().max(), 54 | value=[500, 6000], 55 | ) 56 | 57 | ui.input_action_button("reset", "Reset Slider", class_="btn-primary") 58 | 59 | with ui.layout_columns(): 60 | 61 | with ui.card(): 62 | ui.card_header("Model Metrics") 63 | 64 | @render_plotly 65 | def metric_plot(): 66 | if input.metric() == "ROC Curve": 67 | return plot_auc_curve( 68 | character_filter(), "is_electronics", "training_score" 69 | ) 70 | else: 71 | return plot_precision_recall_curve( 72 | character_filter(), "is_electronics", "training_score" 73 | ) 74 | 75 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 76 | 77 | with ui.card(): 78 | ui.card_header("Model Scores") 79 | 80 | @render_plotly 81 | def score_dist(): 82 | return plot_score_distribution(character_filter()) 83 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.2-reactive-effect/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import render, ui, input 2 | from shiny import reactive 3 | from data_import import df 4 | from plots import plot_auc_curve, plot_precision_recall_curve, plot_score_distribution 5 | from shinywidgets import render_plotly 6 | 7 | 8 | @reactive.calc() 9 | def account_data(): 10 | return df[ 11 | (df["account"] == input.account()) & (df["sub_account"] == input.sub_account()) 12 | ] 13 | 14 | 15 | @reactive.calc() 16 | def character_filter(): 17 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 18 | 19 | 20 | with ui.sidebar(): 21 | ui.input_select( 22 | "account", 23 | "Account", 24 | choices=[ 25 | "Berge & Berge", 26 | "Fritsch & Fritsch", 27 | "Hintz & Hintz", 28 | "Mosciski and Sons", 29 | "Wolff Ltd", 30 | ], 31 | ) 32 | 33 | @render.express 34 | def sub_selector(): 35 | # We can't use the account_data reactive here because we are using the 36 | # sub_account input as part of that reactive. 37 | choice_data = df[df["account"] == input.account()] 38 | choices = choice_data["sub_account"].unique().tolist() 39 | ui.input_select("sub_account", "Sub Account", choices=choices) 40 | 41 | ui.input_slider( 42 | "chars", 43 | "Text length", 44 | min=0, 45 | max=df["text"].str.len().max(), 46 | value=[500, 6000], 47 | ) 48 | 49 | with ui.layout_columns(): 50 | 51 | with ui.card(): 52 | ui.card_header("Model Metrics") 53 | 54 | @render_plotly 55 | def metric_plot(): 56 | if input.metric() == "ROC Curve": 57 | return plot_auc_curve( 58 | character_filter(), "is_electronics", "training_score" 59 | ) 60 | else: 61 | return plot_precision_recall_curve( 62 | character_filter(), "is_electronics", "training_score" 63 | ) 64 | 65 | ui.input_select("metric", "Metric", choices=["ROC Curve", "Precision Recall"]) 66 | 67 | with ui.card(): 68 | ui.card_header("Model Scores") 69 | 70 | @render_plotly 71 | def score_dist(): 72 | return plot_score_distribution(character_filter()) 73 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.2-reactive-effect/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | import numpy as np 4 | 5 | file_path = Path(__file__).parent / "simulated-data.csv" 6 | 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.2-reactive-effect/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | 8 | import plotly.io as pio 9 | 10 | # Set the default plotly theme to resemble ggplot's theme_light 11 | pio.templates.default = "plotly_white" 12 | 13 | 14 | def plot_score_distribution(df: DataFrame): 15 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 16 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 17 | return fig 18 | 19 | 20 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 21 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 22 | roc_auc = auc(fpr, tpr) 23 | 24 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 25 | 26 | fig = px.line( 27 | roc_df, 28 | x="False Positive Rate", 29 | y="True Positive Rate", 30 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 31 | labels={ 32 | "False Positive Rate": "False Positive Rate", 33 | "True Positive Rate": "True Positive Rate", 34 | }, 35 | ) 36 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 37 | return fig 38 | 39 | 40 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 41 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 42 | 43 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 44 | 45 | fig = px.line( 46 | pr_df, 47 | x="Recall", 48 | y="Precision", 49 | title="Precision-Recall Curve", 50 | labels={"Recall": "Recall", "Precision": "Precision"}, 51 | ) 52 | return fig 53 | 54 | 55 | def plot_api_response(df): 56 | account = df["account"].unique() 57 | 58 | data = np.random.lognormal(0, 1 / len(account), 10000) 59 | df = pd.DataFrame({"Value": data}) 60 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 61 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 62 | return fig 63 | -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/5.2-reactive-effect/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/5-reactive-effects/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/problem-sets/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly -------------------------------------------------------------------------------- /apps/target-app/app.py: -------------------------------------------------------------------------------- 1 | from numpy import char, place 2 | from shiny.express import ui, input, render 3 | from shiny import reactive 4 | 5 | from plots import ( 6 | plot_score_distribution, 7 | plot_auc_curve, 8 | plot_precision_recall_curve, 9 | ) 10 | import faicons as fa 11 | import io 12 | from shinywidgets import render_plotly 13 | from data_import import df 14 | 15 | 16 | @reactive.calc() 17 | def account_data(): 18 | return df[ 19 | (df["account"] == input.account()) & (df["sub_account"] == input.sub_account()) 20 | ] 21 | 22 | 23 | @reactive.calc() 24 | def character_filter(): 25 | return account_data()[(account_data()["text"].str.len().between(*input.chars()))] 26 | 27 | 28 | @reactive.effect 29 | @reactive.event(input.reset) 30 | def reset_vals(): 31 | ui.update_slider("chars", value=[500, 6000]) 32 | 33 | 34 | with ui.sidebar(): 35 | ui.input_select( 36 | "account", 37 | "Account", 38 | choices=[ 39 | "Berge & Berge", 40 | "Fritsch & Fritsch", 41 | "Hintz & Hintz", 42 | "Mosciski and Sons", 43 | "Wolff Ltd", 44 | ], 45 | ) 46 | 47 | @render.express 48 | def sub_selector(): 49 | choice_data = df[df["account"] == input.account()] 50 | choices = choice_data["sub_account"].unique().tolist() 51 | ui.input_select("sub_account", "Sub Account", choices=choices) 52 | 53 | with ui.tooltip(id="btn_tooltip", placement="right"): 54 | ui.input_slider( 55 | "chars", 56 | "Text length", 57 | min=0, 58 | max=df["text"].str.len().max(), 59 | value=[500, 6000], 60 | ) 61 | "The number of characters in the text" 62 | 63 | ui.input_action_button("reset", "Reset Values", class_="btn-primary") 64 | 65 | with ui.nav_panel("Training Dashboard"): 66 | with ui.layout_columns(): 67 | with ui.card(): 68 | ui.card_header("Model Metrics") 69 | 70 | @render_plotly 71 | def metric_plot(): 72 | if input.metric() == "ROC Curve": 73 | return plot_auc_curve( 74 | character_filter(), "is_electronics", "training_score" 75 | ) 76 | else: 77 | return plot_precision_recall_curve( 78 | character_filter(), "is_electronics", "training_score" 79 | ) 80 | 81 | ui.input_select( 82 | "metric", "Metric", choices=["ROC Curve", "Precision Recall"] 83 | ) 84 | 85 | with ui.card(): 86 | ui.card_header("Model Scores") 87 | 88 | @render_plotly 89 | def score_dist(): 90 | return plot_score_distribution(character_filter()) 91 | 92 | 93 | vb_theme = "bg-blue" 94 | 95 | with ui.nav_panel("Data"): 96 | with ui.layout_columns(): 97 | with ui.value_box(theme=vb_theme): 98 | "Number of records" 99 | 100 | @render.text 101 | def data_count(): 102 | return str(character_filter().shape[0]) 103 | 104 | with ui.value_box(theme=vb_theme): 105 | "Mean Score" 106 | 107 | @render.text 108 | def mean_score(theme=vb_theme): 109 | return f"{character_filter()['training_score'].mean() * 100:.2f}%" 110 | 111 | with ui.value_box(theme=vb_theme): 112 | "Mean Text Length" 113 | 114 | @render.text 115 | def mean_text_length(): 116 | return f"{character_filter()['text'].str.len().mean():.2f} characters" 117 | 118 | with ui.card(full_screen=True): 119 | with ui.card_header(): 120 | "Data" 121 | with ui.popover( 122 | title="Download", 123 | class_="d-inline-block pull-right", 124 | ): 125 | fa.icon_svg("download") 126 | 127 | @render.download(filename=lambda: f"{input.account()}_scores.csv") 128 | def download_data(): 129 | with io.BytesIO() as buf: 130 | account_data().to_csv(buf, index=False) 131 | buf.seek(0) 132 | yield buf.getvalue() 133 | 134 | @render.data_frame 135 | def data_output(): 136 | return account_data().drop(columns=["text"]) 137 | -------------------------------------------------------------------------------- /apps/target-app/data_import.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pandas as pd 3 | 4 | file_path = Path(__file__).parent / "simulated-data.csv" 5 | 6 | print("Running!") 7 | df = pd.read_csv(file_path, dtype={"sub_account": str}) 8 | df["date"] = pd.to_datetime(df["date"], errors="coerce") 9 | -------------------------------------------------------------------------------- /apps/target-app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "locale": "en_US.UTF-8", 4 | "metadata": { 5 | "appmode": "python-shiny", 6 | "entrypoint": "app" 7 | }, 8 | "python": { 9 | "version": "3.11.4", 10 | "package_manager": { 11 | "name": "pip", 12 | "version": "24.0", 13 | "package_file": "requirements.txt" 14 | } 15 | }, 16 | "files": { 17 | "requirements.txt": { 18 | "checksum": "d40adc5fb1d3a2fbf665f73725689501" 19 | }, 20 | "app.py": { 21 | "checksum": "876a7c1b6a172fc84add63b9b0e295bd" 22 | }, 23 | "plots.py": { 24 | "checksum": "b517859f4d6a8a1f59b00bdd6c12bccc" 25 | }, 26 | "simulated-data.csv": { 27 | "checksum": "90a2519c364580b74d0c40e568c182ee" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/target-app/plots.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from pandas import DataFrame 3 | import pandas as pd 4 | from sklearn.metrics import roc_curve, auc, precision_recall_curve 5 | import numpy as np 6 | 7 | import plotly.io as pio 8 | 9 | # Set the default plotly theme to resemble ggplot's theme_light 10 | pio.templates.default = "plotly_white" 11 | 12 | 13 | def plot_score_distribution(df: DataFrame): 14 | fig = px.histogram(df, x="training_score", nbins=50, title="Model scores") 15 | fig.update_layout(xaxis_title="Score", yaxis_title="Density") 16 | return fig 17 | 18 | 19 | def plot_auc_curve(df: DataFrame, true_col: str, pred_col: str): 20 | fpr, tpr, _ = roc_curve(df[true_col], df[pred_col]) 21 | roc_auc = auc(fpr, tpr) 22 | 23 | roc_df = DataFrame({"False Positive Rate": fpr, "True Positive Rate": tpr}) 24 | 25 | fig = px.line( 26 | roc_df, 27 | x="False Positive Rate", 28 | y="True Positive Rate", 29 | title=f"Receiver Operating Characteristic (ROC) - AUC: {roc_auc.round(2)}", 30 | labels={ 31 | "False Positive Rate": "False Positive Rate", 32 | "True Positive Rate": "True Positive Rate", 33 | }, 34 | ) 35 | fig.add_shape(type="line", line=dict(dash="dash"), x0=0, x1=1, y0=0, y1=1) 36 | return fig 37 | 38 | 39 | def plot_precision_recall_curve(df: DataFrame, true_col: str, pred_col: str): 40 | precision, recall, _ = precision_recall_curve(df[true_col], df[pred_col]) 41 | 42 | pr_df = DataFrame({"Recall": recall, "Precision": precision}) 43 | 44 | fig = px.line( 45 | pr_df, 46 | x="Recall", 47 | y="Precision", 48 | title="Precision-Recall Curve", 49 | labels={"Recall": "Recall", "Precision": "Precision"}, 50 | ) 51 | return fig 52 | 53 | 54 | def plot_api_response(df): 55 | account = df["account"].unique() 56 | 57 | data = np.random.lognormal(0, 1 / len(account), 10000) 58 | df = pd.DataFrame({"Value": data}) 59 | fig = px.histogram(df, x="Value", nbins=50, title="API response time") 60 | fig.update_layout(xaxis_title="Seconds", yaxis_title="Density") 61 | return fig 62 | -------------------------------------------------------------------------------- /apps/target-app/requirements.txt: -------------------------------------------------------------------------------- 1 | shiny==0.8.1 2 | pandas 3 | plotly 4 | shinywidgets==0.3.1 5 | scikit-learn 6 | faicons -------------------------------------------------------------------------------- /apps/target-app/rsconnect-python/target-app.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://api.shinyapps.io": { 3 | "server_url": "https://api.shinyapps.io", 4 | "filename": "/Users/gordon/Documents/presentations/shiny-python-workshop-2023/apps/target-app", 5 | "app_url": "https://gordonposit.shinyapps.io/target-app/", 6 | "app_id": 11543078, 7 | "app_guid": null, 8 | "title": "target-app", 9 | "app_mode": "python-shiny", 10 | "app_store_version": 1 11 | } 12 | } -------------------------------------------------------------------------------- /apps/utilities/multiple-choice/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui, reactive, req 2 | import json 3 | from pathlib import Path 4 | 5 | with open(Path(__file__).parent / "questions.json", "r") as file: 6 | questions = json.load(file) 7 | 8 | app_ui = ui.page_fluid(ui.output_ui("question"), ui.output_ui("validation")) 9 | 10 | 11 | def server(input, output, session): 12 | q_num = reactive.Value(0) 13 | 14 | question_keys = list(questions.keys()) 15 | 16 | @reactive.Calc 17 | def current_q(): 18 | return questions[question_keys[q_num()]] 19 | 20 | @output 21 | @render.ui 22 | def question(): 23 | return ui.div( 24 | ui.h5(question_keys[q_num()], style="margin-bottom: 20px"), 25 | ui.input_radio_buttons( 26 | "question", 27 | "", 28 | choices=current_q()["choices"], 29 | selected="", 30 | ), 31 | ) 32 | 33 | @output 34 | @render.ui 35 | def validation(): 36 | req(input.question()) 37 | if input.question() == current_q()["answer"]: 38 | if q_num() == len(question_keys) - 1: 39 | return ui.p("Correct! Quiz complete.") 40 | else: 41 | return ui.div( 42 | ui.p("Correct!"), ui.input_action_button("next", "Next Question") 43 | ) 44 | return ui.p("Not quite right, try again") 45 | 46 | @reactive.Effect 47 | @reactive.event(input.next) 48 | def next_question(): 49 | q_num.set(q_num.get() + 1) 50 | 51 | 52 | app = App(app_ui, server) 53 | -------------------------------------------------------------------------------- /apps/utilities/multiple-choice/questions.json: -------------------------------------------------------------------------------- 1 | { 2 | "What is your favourite color?": { 3 | "choices": [ 4 | "blue", 5 | "green", 6 | "gold" 7 | ], 8 | "answer": "green" 9 | }, 10 | "What is your favourite food?": { 11 | "choices": [ 12 | "beans", 13 | "apples", 14 | "alfalfa" 15 | ], 16 | "answer": "beans" 17 | } 18 | } -------------------------------------------------------------------------------- /exercises/1-hello-world.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hello Shiny" 3 | --- 4 | 5 | These exercises should be done after Chapter 2 of the course. 6 | 7 | ```{python} 8 | # | echo: false 9 | import os 10 | 11 | os.chdir("..") 12 | from helpers import problem_tabs 13 | 14 | ``` 15 | 16 | 17 | ```{python} 18 | # | echo: false 19 | # | output: asis 20 | 21 | problem_tabs("1.0-hello-world") 22 | ``` 23 | 24 | 25 | ```{python} 26 | # | echo: false 27 | # | output: asis 28 | 29 | problem_tabs("1.1-data-frame") 30 | ``` 31 | 32 | ```{python} 33 | # | echo: false 34 | # | output: asis 35 | 36 | problem_tabs("1.2-debug") 37 | ``` 38 | 39 | ```{python} 40 | # | echo: false 41 | # | output: asis 42 | 43 | problem_tabs("1.3-filter-input") 44 | ``` 45 | 46 | ```{python} 47 | # | echo: false 48 | # | output: asis 49 | 50 | problem_tabs("1.4-filter-connect") 51 | ``` 52 | 53 | ```{python} 54 | # | echo: false 55 | # | output: asis 56 | 57 | problem_tabs("1.5-debug") 58 | ``` 59 | 60 | ```{python} 61 | # | echo: false 62 | # | output: asis 63 | 64 | problem_tabs("1.6-debug") 65 | ``` 66 | 67 | ```{python} 68 | # | echo: false 69 | # | output: asis 70 | 71 | problem_tabs("1.7-add-plot") 72 | ``` -------------------------------------------------------------------------------- /exercises/2-basic-ui.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Basic UI" 3 | --- 4 | These exercises should be done after Chapter 3 of the course. 5 | ```{python} 6 | # | echo: false 7 | import os 8 | 9 | os.chdir("..") 10 | from helpers import problem_tabs 11 | 12 | ``` 13 | 14 | ```{python} 15 | # | echo: false 16 | # | output: asis 17 | 18 | problem_tabs("2.1-sidebar") 19 | ``` 20 | 21 | ```{python} 22 | # | echo: false 23 | # | output: asis 24 | 25 | problem_tabs("2.2-cards") 26 | ``` 27 | 28 | ```{python} 29 | # | echo: false 30 | # | output: asis 31 | 32 | problem_tabs("2.3-cards-switch") 33 | ``` 34 | 35 | ```{python} 36 | # | echo: false 37 | # | output: asis 38 | 39 | problem_tabs("2.4-layout-columns") 40 | ``` -------------------------------------------------------------------------------- /exercises/3-reactivity.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reactivity" 3 | --- 4 | These exercises should be done after Chapter 4 of the course. 5 | ```{python} 6 | # | echo: false 7 | import os 8 | 9 | os.chdir("..") 10 | from helpers import problem_tabs 11 | 12 | ``` 13 | 14 | ```{python} 15 | # | echo: false 16 | # | output: asis 17 | 18 | problem_tabs("3.1-reactive-calc") 19 | ``` 20 | 21 | ```{python} 22 | # | echo: false 23 | # | output: asis 24 | 25 | problem_tabs("3.2-stacking-reactives") 26 | ``` -------------------------------------------------------------------------------- /exercises/4-dynamic-ui.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Dynamic UI" 3 | --- 4 | These exercises should be done after Chapter 5 of the course. 5 | ```{python} 6 | # | echo: false 7 | import os 8 | 9 | os.chdir("..") 10 | from helpers import problem_tabs 11 | 12 | ``` 13 | 14 | ```{python} 15 | # | echo: false 16 | # | output: asis 17 | 18 | problem_tabs("4.1-render-express") 19 | ``` -------------------------------------------------------------------------------- /exercises/5-reactive-effect.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reactive Effects" 3 | --- 4 | These exercises should be done after Chapter 6 of the course. 5 | ```{python} 6 | # | echo: false 7 | import os 8 | 9 | os.chdir("..") 10 | from helpers import problem_tabs 11 | 12 | ``` 13 | 14 | ```{python} 15 | # | echo: false 16 | # | output: asis 17 | 18 | problem_tabs("5.1-reactive-event") 19 | ``` 20 | 21 | 22 | ```{python} 23 | # | echo: false 24 | # | output: asis 25 | 26 | problem_tabs("5.2-reactive-effect") 27 | ``` -------------------------------------------------------------------------------- /helpers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import glob 3 | import tempfile 4 | import shutil 5 | import os 6 | import json 7 | 8 | 9 | class QuartoPrint(list): 10 | def __init__(self, data): 11 | super().__init__(data) 12 | 13 | def __str__(self): 14 | return "\n".join(str(item) for item in self) 15 | 16 | def append_file(self, file_path: str, file_name: str = None): 17 | if file_name is not None: 18 | self.append(f"## file: {file_name}") 19 | 20 | with open(file_path, "r") as app_file: 21 | app_contents = app_file.read() 22 | self.append(app_contents) 23 | 24 | 25 | def list_files(path: str) -> list: 26 | files = glob.glob(path + "/**", recursive=True) 27 | files = [file for file in files if not glob.os.path.isdir(file)] 28 | return files 29 | 30 | 31 | def include_shiny_folder( 32 | path: str, 33 | file_name: str = "app.py", 34 | exclusions: list = [], 35 | components: str = "editor, viewer", 36 | viewer_height: str = "800", 37 | extra_object: any = "", 38 | ) -> None: 39 | print( 40 | _include_shiny_folder( 41 | path, file_name, exclusions, components, viewer_height, extra_object 42 | ) 43 | ) 44 | 45 | 46 | def _include_shiny_folder( 47 | path: str, 48 | file_name: str = "app.py", 49 | exclusions: list = [], 50 | components: str = "editor, viewer", 51 | viewer_height: str = "800", 52 | extra_object: any = "", 53 | ) -> QuartoPrint: 54 | folder_path = Path(__name__).parent / path 55 | 56 | # Start with the header 57 | block = QuartoPrint( 58 | [ 59 | "```{shinylive-python}", 60 | "#| standalone: true", 61 | f"#| components: [{components}]", 62 | "#| layout: horizontal", 63 | f"#| viewerHeight: {viewer_height}", 64 | ] 65 | ) 66 | 67 | # Print contents of the main application 68 | block.append_file(folder_path / file_name, None) 69 | 70 | exclude_list = ["__pycache__"] + [file_name] + exclusions 71 | 72 | files = list_files(path) 73 | 74 | path_list = [ 75 | string 76 | for string in files 77 | if not any(exclusion in string for exclusion in exclude_list) 78 | ] 79 | 80 | file_names = [string.replace(f"{str(folder_path)}/", "") for string in path_list] 81 | 82 | # Additional files need to start with ## file: 83 | for x, y in zip(path_list, file_names): 84 | block.append_file(x, y) 85 | 86 | # Finish with the closing tag 87 | block.append("```") 88 | return block 89 | 90 | 91 | def collapse_prompt(prompt: str) -> list: 92 | out = [ 93 | "", 94 | '::: {.callout-note collapse="false"}', 95 | "## Exercise", 96 | prompt, 97 | ":::", 98 | "", 99 | ] 100 | return out 101 | 102 | 103 | def parse_readme(path: str) -> str: 104 | file_path = Path(__name__).parent / path / "README" 105 | file_contents = "" 106 | with open(file_path, "r") as file: 107 | file_contents = file.read() 108 | return file_contents 109 | 110 | 111 | def problem_tabs(folder_name: str) -> None: 112 | 113 | import os 114 | 115 | def find_problem_set_folder(base_path, target_path): 116 | for root, dirs, files in os.walk(base_path): 117 | for name in dirs: 118 | full_path = os.path.join(root, name) 119 | if target_path in full_path: 120 | return full_path 121 | raise FileNotFoundError( 122 | f"Folder matching path '{target_path}' not found in '{base_path}'." 123 | ) 124 | 125 | path = find_problem_set_folder("apps/problem-sets", folder_name) 126 | 127 | formatted_title = "## " + folder_name.replace("-", " ").title() 128 | 129 | block = QuartoPrint( 130 | [ 131 | formatted_title, 132 | "::::: {.column-screen-inset-right}", 133 | "::: {.panel-tabset}", 134 | "## Goal", 135 | ] 136 | ) 137 | prompt = parse_readme(path) 138 | block.extend(collapse_prompt(prompt)) 139 | block.extend( 140 | _include_shiny_folder( 141 | path, 142 | "app-solution.py", 143 | exclusions=["app.py", "README"], 144 | components="viewer", 145 | ) 146 | ) 147 | block.append("## Problem") 148 | block.extend(collapse_prompt(prompt)) 149 | block.extend( 150 | _include_shiny_folder(path, "app.py", exclusions=["app-solution.py", "README"]) 151 | ) 152 | block.append("## Solution") 153 | block.extend(collapse_prompt(prompt)) 154 | block.extend( 155 | _include_shiny_folder(path, "app-solution.py", exclusions=["app.py", "README"]) 156 | ) 157 | block.append("## {{< bi github >}}") 158 | block.append( 159 | f"The source code for this exercise is [here](https://github.com/talkpython/reactive-web-dashboards-with-shiny-course/tree/main/{path})." 160 | ) 161 | 162 | block.append(":::") 163 | block.append(":::::") 164 | print(block) 165 | 166 | 167 | class Quiz(dict): 168 | def __init__(self, data): 169 | super().__init__(data) 170 | self.validate() 171 | 172 | def validate(self): 173 | if not isinstance(self, dict): 174 | raise ValueError("Invalid data format: The data should be a dictionary.") 175 | for key, value in self.items(): 176 | if not isinstance(value, dict): 177 | raise ValueError( 178 | f"Invalid data format for '{key}': The value should be a dictionary." 179 | ) 180 | if "choices" not in value or "answer" not in value: 181 | raise ValueError( 182 | f"Invalid data format for '{key}': Missing 'choices' or 'answer' key." 183 | ) 184 | if not isinstance(value["choices"], list) or not all( 185 | isinstance(choice, str) for choice in value["choices"] 186 | ): 187 | raise ValueError( 188 | f"Invalid data format for '{key}': 'choices' should be a list of strings." 189 | ) 190 | if not isinstance(value["answer"], str): 191 | raise ValueError( 192 | f"Invalid data format for '{key}': 'answer' should be a string." 193 | ) 194 | if value["answer"] not in value["choices"]: 195 | raise ValueError( 196 | f"Invalid data format for '{key}': '{value['answer']}' is not one of the choices." 197 | ) 198 | 199 | return True 200 | 201 | 202 | def multiple_choice_app(questions: Quiz): 203 | questions = Quiz(questions) 204 | temp_dir = tempfile.mkdtemp("temp_folder") 205 | shutil.copy("apps/utilities/multiple-choice/app.py", temp_dir) 206 | with open(os.path.join(temp_dir, "questions.json"), "w") as file: 207 | json.dump(questions, file) 208 | 209 | print("::: callout-note") 210 | print("## Test your understanding") 211 | include_shiny_folder(temp_dir, components="viewer", viewer_height="250") 212 | print(":::") 213 | -------------------------------------------------------------------------------- /images/git-download-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/git-download-button.png -------------------------------------------------------------------------------- /images/shiny-course_image/shiny-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny-2x.png -------------------------------------------------------------------------------- /images/shiny-course_image/shiny-2x.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny-2x.webp -------------------------------------------------------------------------------- /images/shiny-course_image/shiny.pxd/QuickLook/Icon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny.pxd/QuickLook/Icon.webp -------------------------------------------------------------------------------- /images/shiny-course_image/shiny.pxd/QuickLook/Thumbnail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny.pxd/QuickLook/Thumbnail.webp -------------------------------------------------------------------------------- /images/shiny-course_image/shiny.pxd/data/3F96BFC4-F9B9-4D8F-A3A9-DEE75FBCD65C: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny.pxd/data/3F96BFC4-F9B9-4D8F-A3A9-DEE75FBCD65C -------------------------------------------------------------------------------- /images/shiny-course_image/shiny.pxd/data/AF550587-953F-4997-9677-0C075842D366-76320-00013F899D49BA33: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny.pxd/data/AF550587-953F-4997-9677-0C075842D366-76320-00013F899D49BA33 -------------------------------------------------------------------------------- /images/shiny-course_image/shiny.pxd/metadata.info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/reactive-web-dashboards-with-shiny-course/2cbf3888e6e03a37a1a6743a0c7a203cf581b73f/images/shiny-course_image/shiny.pxd/metadata.info -------------------------------------------------------------------------------- /import-helpers.quarto_ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "metadata": {}, 6 | "source": [ 7 | "# | echo: false\n", 8 | "import os\n", 9 | "\n", 10 | "os.chdir(\"..\")\n", 11 | "from helpers import problem_tabs, include_shiny_folder\n", 12 | "\n", 13 | "print(\"foo!\")" 14 | ], 15 | "id": "018ca038", 16 | "execution_count": null, 17 | "outputs": [] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "name": "python3", 23 | "language": "python", 24 | "display_name": "Python 3 (ipykernel)", 25 | "path": "/Users/gordon/Documents/presentations/shiny-python-workshop-2023/.venv/share/jupyter/kernels/python3" 26 | } 27 | }, 28 | "nbformat": 4, 29 | "nbformat_minor": 5 30 | } -------------------------------------------------------------------------------- /index.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Reactive Web Dashboards with Shiny" 3 | --- 4 | 5 | # Web applications without the headaches 6 | 7 | Shiny lets you build beautiful, performant dashboards and web applications without a lot of effort. 8 | Instead of manually managing callback functions and application state, Shiny uses a reactive framework to automatically determine the best way to run your application. 9 | This means that you can focus on communicating insights rather than debugging your web application. 10 | 11 | This site is a companion to the [Talk Python video course](https://training.talkpython.fm/purchase/access_code/f3180ced-3d5c-42ad-a318-e81e6355a4fd), and provides some exercises that let you practice what you learned in the videos. 12 | 13 | # Who is this course for? 14 | 15 | This course assumes that you have a basic understanding of Python, but does not require any knowledge of web applications, JavaScript, or HTML. 16 | If you can read a CSV, manipulate data, and draw a plot in Python, then you have everything you need to start building a Shiny application. 17 | 18 | # Setup 19 | 20 | There are two ways to work through the examples and exercises in this workshop. 21 | 22 | ## 1) In your web browser with Shinylive 23 | 24 | Shinylive allows you to run full-featured shiny apps in your browser, and includes a basic editor which is good enough to run examples. 25 | As a result you should be able to work through all of the examples in the workshop using just the browser without installing anything locally. 26 | 27 | ## 2) Locally with VS Code 28 | 29 | While Shinylive is great, it likely isn't the environment you'll use to develop Shiny apps, and so it makes sense to set up VS Code and run the examples locally. To do this, follow these steps before the workshop: 30 | 31 | 1) Install [VS Code](https://code.visualstudio.com/) 32 | 2) Install the [Shiny extension for VS Code](https://marketplace.visualstudio.com/items?itemName=posit.shiny) 33 | 3) Clone the repository with `git clone https://github.com/rstudio/shiny-python-workshop-2023.git`, or alternately download the repository as a zip file: \ 34 | ![](images/git-download-button.png){width="287"} 35 | 4) Navigate to the project directory and create a new virtual environment with `python3 -m venv .venv` 36 | 5) Set your Python interpreter to the virtual environment with `CMD + SHIFT + P` > `Select Interpreter` 37 | 6) Open a new terminal prompt, which should switch to `(.venv)` 38 | 7) Install the relevant packages with `pip install -r requirements.txt` 39 | 40 | All of the example apps are stored in the `/apps` directory. 41 | The examples are in `apps/examples` and the problem sets are in `apps/problem-sets`. 42 | If you've installed the Shiny extension for VS Code, you can run any of the apps by opening the `app.py` file and clicking the play button in the top right. ([See screenshot](https://camo.githubusercontent.com/5d947e6dff7d74fd1cf221e79583105c42e4986ae673ce79733ce5edbfdcdda5/68747470733a2f2f7368696e792e7273747564696f2e636f6d2f70792f646f63732f6173736574732f7673636f64652e706e67)) 43 | 44 | Alternatively, run them from the command line with `shiny run --reload`. 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | shiny 2 | shinylive 3 | pandas 4 | plotnine 5 | seaborn 6 | jupyter -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* css styles */ 2 | 3 | #vcenter { 4 | vertical-align: middle; 5 | } 6 | 7 | .callout .shinylive-wrapper { 8 | border: none; 9 | box-shadow: none; 10 | margin: 0; 11 | } 12 | 13 | .someclass { 14 | fill: rgb(240, 102, 255); 15 | } 16 | 17 | .panel-tabset .shinylive-container { 18 | max-height: calc(80vh - 110px); 19 | overflow: auto; 20 | margin-bottom: 20px; 21 | } 22 | 23 | .v-center-container { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | height: 90%; 28 | background-color: brown; 29 | } 30 | 31 | .reveal div.column { 32 | background-color: blueviolet; 33 | } --------------------------------------------------------------------------------