├── .envrc ├── .github └── workflows │ ├── build.yml │ └── update_shiny.yml ├── .gitignore ├── .gitmodules ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── _shinylive ├── _redirects └── favicon.ico ├── examples ├── index.json ├── python │ ├── altair │ │ ├── about.txt │ │ ├── app.py │ │ └── requirements.txt │ ├── app_with_plot │ │ ├── about.txt │ │ └── app.py │ ├── basic_app │ │ ├── about.txt │ │ └── app.py │ ├── brand │ │ ├── Monda-OFL.txt │ │ ├── Monda.ttf │ │ ├── _brand.yml │ │ ├── _colors.scss │ │ ├── about.txt │ │ ├── app.py │ │ └── requirements.txt │ ├── camera │ │ ├── about.txt │ │ └── app.py │ ├── cpuinfo │ │ ├── about.txt │ │ ├── app.py │ │ ├── fakepsutil.py │ │ └── helpers.py │ ├── extra_packages │ │ ├── about.txt │ │ ├── app.py │ │ └── requirements.txt │ ├── fetch │ │ ├── about.txt │ │ ├── app.py │ │ └── download.py │ ├── file_download │ │ ├── about.txt │ │ ├── app.py │ │ └── mtcars.csv │ ├── file_download_core │ │ ├── about.txt │ │ ├── app.py │ │ └── mtcars.csv │ ├── file_upload │ │ ├── about.txt │ │ └── app.py │ ├── hello_world │ │ ├── about.txt │ │ └── hello.py │ ├── input_checkbox │ │ ├── about.txt │ │ └── app.py │ ├── input_checkbox_group │ │ ├── about.txt │ │ └── app.py │ ├── input_date │ │ ├── about.txt │ │ └── app.py │ ├── input_date_range │ │ ├── about.txt │ │ └── app.py │ ├── input_numeric │ │ ├── about.txt │ │ └── app.py │ ├── input_password │ │ ├── about.txt │ │ └── app.py │ ├── input_radio │ │ ├── about.txt │ │ └── app.py │ ├── input_select │ │ ├── about.txt │ │ └── app.py │ ├── input_slider │ │ ├── about.txt │ │ └── app.py │ ├── input_switch │ │ ├── about.txt │ │ └── app.py │ ├── input_text │ │ ├── about.txt │ │ └── app.py │ ├── input_text_area │ │ ├── about.txt │ │ └── app.py │ ├── input_update │ │ ├── about.txt │ │ └── app.py │ ├── insert_ui │ │ ├── about.txt │ │ └── app.py │ ├── ipyleaflet │ │ ├── about.txt │ │ └── app.py │ ├── layout_sidebar │ │ ├── about.txt │ │ └── app.py │ ├── layout_two_column │ │ ├── about.txt │ │ └── app.py │ ├── modules │ │ ├── about.txt │ │ └── app.py │ ├── multiple_source_files │ │ ├── about.txt │ │ ├── app.py │ │ └── utils.py │ ├── orbit │ │ ├── about.txt │ │ ├── app.py │ │ ├── body.py │ │ ├── requirements.txt │ │ ├── simulation.py │ │ └── www │ │ │ └── coords.png │ ├── output_code │ │ ├── about.txt │ │ └── app.py │ ├── output_data_frame_grid │ │ ├── about.txt │ │ └── app.py │ ├── output_plot │ │ ├── about.txt │ │ └── app.py │ ├── output_table │ │ ├── about.txt │ │ └── app.py │ ├── output_text │ │ ├── about.txt │ │ └── app.py │ ├── output_ui │ │ ├── about.txt │ │ └── app.py │ ├── plot_interact_basic │ │ ├── about.txt │ │ ├── app.py │ │ └── mtcars.csv │ ├── plot_interact_exclude │ │ ├── about.txt │ │ ├── app.py │ │ └── mtcars.csv │ ├── plot_interact_select │ │ ├── about.txt │ │ ├── app.py │ │ └── mtcars.csv │ ├── plotly │ │ ├── about.txt │ │ ├── app.py │ │ └── requirements.txt │ ├── reactive_calc │ │ ├── about.txt │ │ └── app.py │ ├── reactive_effect │ │ ├── about.txt │ │ └── app.py │ ├── reactive_event │ │ ├── about.txt │ │ └── app.py │ ├── reactive_value │ │ ├── about.txt │ │ └── app.py │ ├── read_local_csv_file │ │ ├── about.txt │ │ ├── app.py │ │ └── mtcars.csv │ ├── regularization │ │ ├── about.txt │ │ ├── app.py │ │ └── compare.py │ ├── shinyswatch │ │ ├── about.txt │ │ ├── app.py │ │ └── requirements.txt │ ├── static_content │ │ ├── about.txt │ │ ├── app.py │ │ └── www │ │ │ └── logo.png │ └── wordle │ │ ├── about.txt │ │ ├── app.py │ │ ├── style.css │ │ └── words.py └── r │ ├── 001-hello │ ├── about.txt │ └── app.R │ ├── 002-text │ ├── about.txt │ └── app.R │ ├── 003-reactivity │ ├── about.txt │ └── app.R │ ├── 004-mpg │ ├── about.txt │ └── app.R │ ├── 005-sliders │ ├── about.txt │ └── app.R │ ├── 006-tabsets │ ├── about.txt │ └── app.R │ ├── 007-widgets │ ├── about.txt │ └── app.R │ ├── 008-html │ ├── about.txt │ ├── app.R │ └── www │ │ └── index.html │ ├── 009-upload │ ├── about.txt │ └── app.R │ ├── 010-download │ ├── about.txt │ └── app.R │ └── 011-timer │ ├── about.txt │ └── app.R ├── export_template ├── edit │ └── index.html └── index.html ├── flake.lock ├── flake.nix ├── package-lock.json ├── package.json ├── playwright.config.ts ├── playwright ├── editor-cell-test │ └── app.py ├── editor-cell.spec.ts ├── examples-viewer.spec.ts ├── helpers.ts ├── load-from-url.spec.ts ├── shiny-static.spec.ts └── static-app-test │ └── app.py ├── pyrightconfig.json ├── requirements-dev.txt ├── scripts ├── build.ts ├── build_examples_json.ts ├── create_typeshed.py └── pyodide_packages.py ├── shinylive_lock.json ├── shinylive_requirements.json ├── site ├── app │ └── .gitignore ├── editor │ └── .gitignore ├── examples │ └── .gitignore ├── favicon.ico ├── shinylive └── shinylive-sw.js ├── site_template ├── app │ └── index.html ├── editor │ └── index.html └── examples │ └── index.html ├── src ├── Components │ ├── App.css │ ├── App.tsx │ ├── Editor.css │ ├── Editor.test.tsx │ ├── Editor.tsx │ ├── ExampleSelector.css │ ├── ExampleSelector.test.tsx │ ├── ExampleSelector.tsx │ ├── HeaderBar.css │ ├── HeaderBar.tsx │ ├── Icons.css │ ├── Icons.tsx │ ├── LoadingAnimation.css │ ├── LoadingAnimation.tsx │ ├── OutputCell.css │ ├── OutputCell.tsx │ ├── ResizableGrid │ │ ├── DragToResizeHelpers.ts │ │ ├── ResizableGrid.css │ │ ├── ResizableGrid.tsx │ │ └── useDragToResizeGrid.tsx │ ├── ShareModal.css │ ├── ShareModal.tsx │ ├── Terminal.css │ ├── Terminal.tsx │ ├── Viewer.css │ ├── Viewer.tsx │ ├── codeMirror │ │ ├── FileTabs.tsx │ │ ├── extensions.ts │ │ ├── language-server │ │ │ ├── autocompletion.ts │ │ │ ├── diagnostics.ts │ │ │ ├── documentation.ts │ │ │ ├── hover.ts │ │ │ ├── lsp-extension.ts │ │ │ ├── names.ts │ │ │ ├── positions.ts │ │ │ ├── regexp-util.ts │ │ │ └── signatureHelp.ts │ │ ├── useTabbedCodeMirror.tsx │ │ └── utils.ts │ ├── filecontent.ts │ ├── gist.ts │ ├── share.ts │ ├── skull.svg │ └── utils.ts ├── assets │ ├── shiny-logo.svg │ └── shinylive-inject-socket.txt ├── awaitable-queue.ts ├── examples.ts ├── fileio.ts ├── fonts │ └── SourceSansPro-Regular.otf.woff2 ├── hooks │ ├── useOnEscOrClickOutside.tsx │ ├── usePyodide.tsx │ ├── useRunOnceOnMount.tsx │ └── useWebR.tsx ├── language-server │ ├── client.ts │ ├── null-client.ts │ └── pyright-client.ts ├── load-shinylive-sw.ts ├── lzstring-worker.ts ├── messageporthttp.ts ├── messageportwebsocket-channel.ts ├── messageportwebsocket.ts ├── parse-codeblock.ts ├── postable-error.ts ├── pyodide-proxy.ts ├── pyodide-worker.ts ├── pyodide │ ├── ffi.d.ts │ ├── pyodide.d.ts │ └── pyodide.js ├── pyright │ ├── PYRIGHT_LICENSE.txt │ ├── README.md │ ├── pyright.worker.js │ └── pyright.worker.js.map ├── run-python-blocks.ts ├── scripts │ └── codeblock-to-json.ts ├── shinylive-inject-socket.ts ├── shinylive-sw.ts ├── style-resets.css ├── types │ └── custom.d.ts ├── utils.ts └── webr-proxy.ts └── tsconfig.json /.envrc: -------------------------------------------------------------------------------- 1 | use flake -------------------------------------------------------------------------------- /.github/workflows/update_shiny.yml: -------------------------------------------------------------------------------- 1 | name: Update py-shiny submodule 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | update-shiny: 8 | name: Update py-shiny submodule 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 3.11 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.11" 19 | 20 | - name: Upgrade pip 21 | run: python -m pip install --upgrade pip 22 | 23 | - name: Check out submodules 24 | run: | 25 | make submodules 26 | 27 | - name: Pull latest py-shiny 28 | run: | 29 | make submodules-pull-shiny 30 | 31 | - name: Build shinylive 32 | run: | 33 | make all 34 | 35 | - name: Set up git within GHA 36 | uses: actions4git/setup-git@v1 37 | 38 | - name: Commit and push changes 39 | run: | 40 | PY_SHINY_SHA=$(git -C packages/py-shiny rev-parse --short HEAD) 41 | git add packages/py-shiny shinylive_lock.json && \ 42 | git commit --message "Pull latest posit-dev/py-shiny@$PY_SHINY_SHA" && \ 43 | git push origin HEAD:main 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | **/.venv/ 3 | **/.DS_store/ 4 | /node_modules/ 5 | /packages/*.whl 6 | /dist/ 7 | /downloads/ 8 | /build/ 9 | __pycache__ 10 | /typings/ 11 | 12 | /test-results/ 13 | /playwright-report/ 14 | /playwright/.cache/ 15 | 16 | .vscode/*.log 17 | 18 | playwright/static-build/ 19 | 20 | /.luarc.json 21 | 22 | _shinylive/py 23 | _shinylive/r 24 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "packages/py-shiny"] 2 | path = packages/py-shiny 3 | url = https://github.com/posit-dev/py-shiny.git 4 | [submodule "packages/py-htmltools"] 5 | path = packages/py-htmltools 6 | url = https://github.com/posit-dev/py-htmltools.git 7 | [submodule "typeshed"] 8 | path = typeshed 9 | url = https://github.com/python/typeshed.git 10 | [submodule "packages/py-shinywidgets"] 11 | path = packages/py-shinywidgets 12 | url = https://github.com/posit-dev/py-shinywidgets.git 13 | [submodule "packages/py-faicons"] 14 | path = packages/py-faicons 15 | url = https://github.com/posit-dev/py-faicons.git 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "[javascript]": { 5 | "editor.formatOnSave": true, 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[typescript]": { 9 | "editor.formatOnSave": true, 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[typescriptreact]": { 13 | "editor.formatOnSave": true, 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[json]": { 17 | "editor.formatOnSave": true, 18 | "editor.defaultFormatter": "esbenp.prettier-vscode" 19 | }, 20 | "[html]": { 21 | "editor.formatOnSave": true, 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "[css]": { 25 | "editor.formatOnSave": true, 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "python.formatting.provider": "black", 29 | "[python]": { 30 | "editor.rulers": [88], 31 | "editor.formatOnSave": true, 32 | "editor.tabSize": 4, 33 | "editor.codeActionsOnSave": { 34 | "source.organizeImports": "explicit" 35 | }, 36 | }, 37 | "isort.args":["--profile", "black"], 38 | "flake8.args": [ 39 | "--ignore=E501,W503" 40 | ], 41 | "eslint.options": { 42 | "reportUnusedDisableDirectives": "error" 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 RStudio, PBC 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 | -------------------------------------------------------------------------------- /_shinylive/_redirects: -------------------------------------------------------------------------------- 1 | / /py/examples/ 302 2 | /py /py/examples/ 302 3 | /py/ /py/examples/ 302 4 | /r /r/examples/ 302 5 | /r/ /r/examples/ 302 6 | -------------------------------------------------------------------------------- /_shinylive/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/shinylive/8acb41b0d4090308551e2f30dccb68fa6c9071a9/_shinylive/favicon.ico -------------------------------------------------------------------------------- /examples/index.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "engine": "python", 4 | "examples": [ 5 | { 6 | "category": "Basic", 7 | "apps": ["basic_app", "app_with_plot"] 8 | }, 9 | { 10 | "category": "Featured", 11 | "apps": ["cpuinfo", "regularization", "plotly", "altair", "ipyleaflet"] 12 | }, 13 | { 14 | "category": "Intermediate", 15 | "apps": [ 16 | "multiple_source_files", 17 | "read_local_csv_file", 18 | "file_upload", 19 | "insert_ui", 20 | "input_update", 21 | "extra_packages", 22 | "fetch", 23 | "brand" 24 | ] 25 | }, 26 | { 27 | "category": "Reactivity", 28 | "apps": [ 29 | "reactive_event", 30 | "reactive_effect", 31 | "reactive_calc", 32 | "reactive_value" 33 | ] 34 | }, 35 | { 36 | "category": "Shiny Core", 37 | "apps": [ 38 | "file_download_core", 39 | "modules", 40 | "orbit", 41 | "wordle", 42 | "static_content" 43 | ] 44 | }, 45 | { 46 | "category": "Interactive plots", 47 | "apps": [ 48 | "plot_interact_basic", 49 | "plot_interact_select", 50 | "plot_interact_exclude" 51 | ] 52 | }, 53 | { 54 | "category": "Non-Apps", 55 | "apps": ["hello_world"] 56 | } 57 | ] 58 | }, 59 | { 60 | "engine": "r", 61 | "examples": [ 62 | { 63 | "category": "R examples", 64 | "apps": [ 65 | "001-hello", 66 | "002-text", 67 | "003-reactivity", 68 | "004-mpg", 69 | "005-sliders", 70 | "006-tabsets", 71 | "007-widgets", 72 | "008-html", 73 | "009-upload", 74 | "010-download", 75 | "011-timer" 76 | ] 77 | } 78 | ] 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /examples/python/altair/about.txt: -------------------------------------------------------------------------------- 1 | altair 2 | Interactive plot with altair 3 | -------------------------------------------------------------------------------- /examples/python/altair/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import input, ui 2 | from shinywidgets import render_altair 3 | 4 | ui.input_selectize("var", "Select variable", choices=["bill_length_mm", "body_mass_g"]) 5 | 6 | 7 | @render_altair 8 | def hist(): 9 | import altair as alt 10 | from palmerpenguins import load_penguins 11 | 12 | df = load_penguins() 13 | return ( 14 | alt.Chart(df) 15 | .mark_bar() 16 | .encode(x=alt.X(f"{input.var()}:Q", bin=True), y="count()") 17 | ) 18 | -------------------------------------------------------------------------------- /examples/python/altair/requirements.txt: -------------------------------------------------------------------------------- 1 | altair 2 | anywidget 3 | palmerpenguins 4 | jsonschema -------------------------------------------------------------------------------- /examples/python/app_with_plot/about.txt: -------------------------------------------------------------------------------- 1 | App with plot 2 | -------------------------------------------------------------------------------- /examples/python/app_with_plot/app.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from shiny.express import ui, input, render 4 | 5 | with ui.sidebar(): 6 | ui.input_slider("n", "N", 0, 100, 20) 7 | 8 | 9 | @render.plot(alt="A histogram") 10 | def histogram(): 11 | np.random.seed(19680801) 12 | x = 100 + 15 * np.random.randn(437) 13 | plt.hist(x, input.n(), density=True) 14 | -------------------------------------------------------------------------------- /examples/python/basic_app/about.txt: -------------------------------------------------------------------------------- 1 | Basic App 2 | -------------------------------------------------------------------------------- /examples/python/basic_app/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import input, render, ui 2 | 3 | ui.input_slider("n", "N", 0, 100, 20) 4 | 5 | 6 | @render.code 7 | def txt(): 8 | return f"n*2 is {input.n() * 2}" 9 | -------------------------------------------------------------------------------- /examples/python/brand/Monda.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/shinylive/8acb41b0d4090308551e2f30dccb68fa6c9071a9/examples/python/brand/Monda.ttf -------------------------------------------------------------------------------- /examples/python/brand/_brand.yml: -------------------------------------------------------------------------------- 1 | # Learn more about brand.yml at https://posit-dev.github.io/brand-yml 2 | meta: 3 | name: 4 | full: "Retro Arcade Brand" 5 | short: "RetroArc" 6 | link: 7 | home: https://retroarc.example.com 8 | mastodon: https://mastodon.social/@retroarc 9 | github: https://github.com/retroarc 10 | linkedin: https://linkedin.com/company/retroarc 11 | twitter: https://twitter.com/retroarc 12 | facebook: https://facebook.com/retroarc 13 | 14 | # This example doesn't include a brand logo 15 | # See: https://posit-dev.github.io/brand-yml/brand/logo.html 16 | # logo: brand-yml.png 17 | 18 | color: 19 | palette: 20 | pink: "#E83E8C" 21 | blue: "#007BFF" 22 | cyan: "#17A2B8" 23 | teal: "#20C997" 24 | green: "#28A745" 25 | yellow: "#FFD700" 26 | orange: "#FF7F50" 27 | red: "#FF3333" 28 | purple: "#6F42C1" 29 | indigo: "#6610F2" 30 | black: "#1A1A1A" 31 | white: "#F8F8F8" 32 | foreground: black 33 | background: white 34 | primary: purple 35 | success: green 36 | info: cyan 37 | warning: yellow 38 | danger: orange 39 | light: white 40 | dark: black 41 | 42 | typography: 43 | fonts: 44 | - family: Quantico 45 | source: google 46 | weight: [700] 47 | style: [normal, italic] 48 | display: swap 49 | - family: Monda 50 | source: file 51 | files: 52 | - path: Monda.ttf 53 | weight: 400..700 54 | - family: Share Tech Mono 55 | source: bunny 56 | weight: 400 57 | style: normal 58 | display: swap 59 | base: 60 | family: Monda 61 | size: 17px 62 | weight: 400 63 | line-height: 1.5 64 | headings: 65 | family: Quantico 66 | weight: 400 67 | line-height: 1.2 68 | style: normal 69 | monospace: 70 | family: Share Tech Mono 71 | size: 0.9em 72 | weight: 400 73 | monospace-inline: 74 | family: Share Tech Mono 75 | # size: 0.9em 76 | weight: 400 77 | color: yellow 78 | background-color: "#1a1a1add" 79 | monospace-block: 80 | family: Share Tech Mono 81 | size: 1.1em 82 | weight: 400 83 | color: green 84 | background-color: black 85 | line-height: 1.4 86 | link: 87 | weight: 400 88 | background-color: purple 89 | color: white 90 | decoration: "underline" 91 | 92 | defaults: 93 | bootstrap: 94 | defaults: 95 | my-pink: "$brand-pink" 96 | shiny: 97 | theme: 98 | preset: shiny 99 | rules: | 100 | .navbar-brand { color: $my-pink } 101 | -------------------------------------------------------------------------------- /examples/python/brand/about.txt: -------------------------------------------------------------------------------- 1 | Branded Theming 2 | Using brand.yml 3 | -------------------------------------------------------------------------------- /examples/python/brand/requirements.txt: -------------------------------------------------------------------------------- 1 | shiny[theme] 2 | matplotlib 3 | numpy 4 | -------------------------------------------------------------------------------- /examples/python/camera/about.txt: -------------------------------------------------------------------------------- 1 | Camera 2 | Use a phone camera for input 3 | -------------------------------------------------------------------------------- /examples/python/camera/app.py: -------------------------------------------------------------------------------- 1 | # This app uses a phone or tablet's camera to take a picture and process it. It cannot 2 | # use a desktop computer's webcam. If opened on a desktop computer, it will open up an 3 | # ordinary file chooser dialog. 4 | # 5 | # This particular application uses some memory-intensive libraries, like skimage, and so 6 | # it may not work properly on all phones. However, the camera input part should still 7 | # work on most phones. 8 | 9 | import numpy as np 10 | import skimage 11 | from PIL import Image, ImageOps 12 | from shiny import App, render, ui 13 | from shiny.types import FileInfo, ImgData, SilentException 14 | 15 | app_ui = ui.page_fluid( 16 | ui.input_file( 17 | "file", 18 | None, 19 | button_label="Open camera", 20 | # This tells it to accept still photos only (not videos). 21 | accept="image/*", 22 | # This tells it to use the phone's rear camera. Use "user" for the front camera. 23 | capture="environment", 24 | ), 25 | ui.output_image("image"), 26 | ) 27 | 28 | 29 | def server(input, output, session): 30 | @render.image 31 | async def image() -> ImgData: 32 | file_infos: list[FileInfo] = input.file() 33 | if not file_infos: 34 | raise SilentException() 35 | 36 | file_info = file_infos[0] 37 | img = Image.open(file_info["datapath"]) 38 | 39 | # Resize to 1000 pixels wide 40 | basewidth = 1000 41 | wpercent = basewidth / float(img.size[0]) 42 | hsize = int((float(img.size[1]) * float(wpercent))) 43 | img = img.resize((basewidth, hsize), Image.ANTIALIAS) 44 | 45 | # Convert to grayscale 46 | img = ImageOps.grayscale(img) 47 | 48 | # Rotate image based on EXIF tag 49 | img = ImageOps.exif_transpose(img) 50 | 51 | # Convert to numpy array for skimage processing 52 | image_data = np.array(img) 53 | 54 | # Apply thresholding 55 | val = skimage.filters.threshold_otsu(image_data) 56 | mask = image_data < val 57 | 58 | # Save for render.image 59 | skimage.io.imsave("small.png", skimage.util.img_as_ubyte(mask)) 60 | return {"src": "small.png", "width": "100%"} 61 | 62 | 63 | app = App(app_ui, server) 64 | -------------------------------------------------------------------------------- /examples/python/cpuinfo/about.txt: -------------------------------------------------------------------------------- 1 | CPU info 2 | CPU load monitor 3 | -------------------------------------------------------------------------------- /examples/python/cpuinfo/fakepsutil.py: -------------------------------------------------------------------------------- 1 | """Generates synthetic data""" 2 | 3 | import numpy as np 4 | 5 | 6 | def cpu_count(logical: bool = True): 7 | return 8 if logical else 4 8 | 9 | 10 | last_sample = np.random.uniform(0, 100, size=cpu_count(True)) 11 | 12 | 13 | def cpu_percent(interval=None, percpu=False): 14 | global last_sample 15 | delta = np.random.normal(scale=10, size=len(last_sample)) 16 | last_sample = (last_sample + delta).clip(0, 100) 17 | if percpu: 18 | return last_sample.tolist() 19 | else: 20 | return last_sample.mean() 21 | -------------------------------------------------------------------------------- /examples/python/cpuinfo/helpers.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def hide_ticks(axis): 9 | for ticks in [axis.get_major_ticks(), axis.get_minor_ticks()]: 10 | for tick in ticks: 11 | tick.tick1line.set_visible(False) 12 | tick.tick2line.set_visible(False) 13 | tick.label1.set_visible(False) 14 | tick.label2.set_visible(False) 15 | 16 | 17 | def plot_cpu(history, nsamples, ncpu, cmap): 18 | if history is None: 19 | history = np.array([]) 20 | history.shape = (ncpu, 0) 21 | 22 | # Throw away samples too old to fit on the plot 23 | if history.shape[1] > nsamples: 24 | history = history[:, -nsamples:] 25 | 26 | ncols = 2 27 | nrows = int(ceil(ncpu / ncols)) 28 | fig, axeses = plt.subplots( 29 | nrows=nrows, 30 | ncols=ncols, 31 | squeeze=False, 32 | ) 33 | for i in range(0, ncols * nrows): 34 | row = i // ncols 35 | col = i % ncols 36 | axes = axeses[row, col] 37 | if i >= len(history): 38 | axes.set_visible(False) 39 | continue 40 | data = history[i] 41 | axes.yaxis.set_label_position("right") 42 | axes.yaxis.tick_right() 43 | axes.set_xlim(-(nsamples - 1), 0) 44 | axes.set_ylim(0, 100) 45 | 46 | assert len(data) <= nsamples 47 | 48 | # Set up an array of x-values that will right-align the data relative to the 49 | # plotting area 50 | x = np.arange(0, len(data)) 51 | x = np.flip(-x) 52 | 53 | # Color bars by cmap 54 | color = plt.get_cmap(cmap)(data / 100) 55 | axes.bar(x, data, color=color, linewidth=0, width=1.0) 56 | 57 | axes.set_yticks([25, 50, 75]) 58 | for ytl in axes.get_yticklabels(): 59 | if col == ncols - 1 or i == ncpu - 1 or True: 60 | ytl.set_fontsize(7) 61 | else: 62 | ytl.set_visible(False) 63 | hide_ticks(axes.yaxis) 64 | for xtl in axes.get_xticklabels(): 65 | xtl.set_visible(False) 66 | hide_ticks(axes.xaxis) 67 | axes.grid(True, linewidth=0.25) 68 | 69 | return fig 70 | -------------------------------------------------------------------------------- /examples/python/extra_packages/about.txt: -------------------------------------------------------------------------------- 1 | Extra packages 2 | Install extra packages from requirements.txt 3 | -------------------------------------------------------------------------------- /examples/python/extra_packages/app.py: -------------------------------------------------------------------------------- 1 | import attrs 2 | import isodate 3 | import tabulate 4 | from shiny.express import ui 5 | 6 | ui.markdown( 7 | """ 8 | This application doesn't actually do anything -- it simply demonstrates how to 9 | import extra packages from PyPI, by using a `requirements.txt` file. 10 | 11 | Packages listed in `requirements.txt` will be installed by micropip just before 12 | the app is started. This means that each time someone visits the app, the 13 | packages will be downloaded and installed into the browser's Pyodide 14 | environment. 15 | 16 | If you want test whether a package can be installed this way, either edit 17 | `requirements.txt` and reload this app, or try running this in the terminal: 18 | 19 | ``` 20 | import micropip 21 | await micropip.install("mypackage") 22 | import mypackage 23 | """ 24 | ) 25 | -------------------------------------------------------------------------------- /examples/python/extra_packages/requirements.txt: -------------------------------------------------------------------------------- 1 | # Packages listed in this file will be installed by micropip just before the app 2 | # is started. This means that each time someone visits the app, the packages 3 | # will be downloaded and installed into the browser's Pyodide environment. 4 | # 5 | # Each line must contain either a requirements specification or the URL for a 6 | # wheel file. 7 | isodate 8 | attrs==21.4.0 9 | httpx [socks,http2] 10 | https://files.pythonhosted.org/packages/ca/80/7c0cad11bd99985cfe7c09427ee0b4f9bd6b048bd13d4ffb32c6db237dfb/tabulate-0.8.9-py3-none-any.whl 11 | -------------------------------------------------------------------------------- /examples/python/fetch/about.txt: -------------------------------------------------------------------------------- 1 | Fetch data from a web API 2 | -------------------------------------------------------------------------------- /examples/python/fetch/app.py: -------------------------------------------------------------------------------- 1 | # Normally in Python, you would use urllib.request.urlopen() to fetch data from a web 2 | # API. However, it won't work in Pyodide because sockets are not available. 3 | # 4 | # Instead, you can pyodide.http.pyfetch(), which is a wrapper for the JavaScript fetch() 5 | # function. Note that when running shinylive, the endpoint MUST use https. This is 6 | # because shinylive must be served over https (unless you are running on localhost), 7 | # and browsers will not allow a https page to fetch data with http. 8 | # 9 | # If you are fetching data from a different origin, that server will also need to allow 10 | # cross-origin requests by setting the Access-Control-Allow-Origin header. See 11 | # https://en.wikipedia.org/wiki/Cross-origin_resource_sharing for more information. 12 | # 13 | # If you see "Failed to fetch" in the Python console, it is likely because the endpoint 14 | # is http and not https, or because the server does not allow cross-origin requests. 15 | # 16 | # One important difference between urllib.request.urlopen() and pyodide.http.pyfetch() 17 | # is that the latter is asynchronous. In a Shiny app, this just means that the 18 | # reactive.calc's and outputs must have `async` in front of the function definitions, 19 | # and when they're called, they must have `await` in front of them. 20 | # 21 | # If you want to write code that works in both regular Python and Pyodide, see the 22 | # download.py file for a wrapper function that can be used to make requests in both 23 | # regular Python and Pyodide. (Note that the function isn't actually used in this app.) 24 | 25 | from pprint import pformat 26 | 27 | import pyodide.http 28 | from shiny import reactive 29 | from shiny.express import input, render, ui 30 | 31 | 32 | @reactive.calc 33 | def url(): 34 | return f"https://goweather.herokuapp.com/weather/{input.city()}" 35 | 36 | 37 | @reactive.calc 38 | async def weather_data(): 39 | if input.city() == "": 40 | return 41 | 42 | response = await pyodide.http.pyfetch(url()) 43 | if response.status != 200: 44 | raise Exception(f"Error fetching {url()}: {response.status}") 45 | 46 | if input.data_type() == "json": 47 | # .json() parses the response as JSON and converts to dictionary. 48 | data = await response.json() 49 | elif input.data_type() == "string": 50 | # .string() returns the response as a string. 51 | data = await response.string() 52 | else: 53 | # .bytes() returns the response as a byte object. 54 | data = await response.bytes() 55 | 56 | return data 57 | 58 | 59 | ui.input_selectize( 60 | "city", 61 | "Select a city:", 62 | [ 63 | "", 64 | "Berlin", 65 | "Cairo", 66 | "Chicago", 67 | "Kyiv", 68 | "London", 69 | "Lima", 70 | "Los Angeles", 71 | "Mexico City", 72 | "Mumbai", 73 | "New York", 74 | "Paris", 75 | "São Paulo", 76 | "Seoul", 77 | "Shanghai", 78 | "Taipei", 79 | "Tokyo", 80 | ], 81 | ) 82 | ui.input_radio_buttons( 83 | "data_type", 84 | "Data conversion type", 85 | { 86 | "json": "Parse JSON and return dict/list", 87 | "string": "String", 88 | "bytes": "Byte object", 89 | }, 90 | ) 91 | 92 | 93 | @render.code 94 | async def info(): 95 | if input.city() == "": 96 | return "" 97 | 98 | data = await weather_data() 99 | if isinstance(data, (str, bytes)): 100 | data_str = data 101 | else: 102 | data_str = pformat(data) 103 | return f"Request URL: {url()}\nResult type: {type(data)}\n{data_str}" 104 | -------------------------------------------------------------------------------- /examples/python/fetch/download.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, Literal 3 | 4 | 5 | class HttpResponse: 6 | def __init__(self, status: int, data: Any): 7 | self.status = status 8 | self.data = data 9 | 10 | 11 | async def get_url( 12 | url: str, type: Literal["string", "bytes", "json"] = "string" 13 | ) -> HttpResponse: 14 | """ 15 | An async wrapper function for http requests that works in both regular Python and 16 | Pyodide. 17 | 18 | In Pyodide, it uses the pyodide.http.pyfetch() function, which is a wrapper for the 19 | JavaScript fetch() function. pyfetch() is asynchronous, so this whole function must 20 | also be async. 21 | 22 | In regular Python, it uses the urllib.request.urlopen() function. 23 | 24 | Args: 25 | url: The URL to download. 26 | 27 | type: How to parse the content. If "string", it returns the response as a 28 | string. If "bytes", it returns the response as a bytes object. If "json", it 29 | parses the reponse as JSON, then converts it to a Python object, usually a 30 | dictionary or list. 31 | 32 | Returns: 33 | A HttpResponse object 34 | """ 35 | import sys 36 | 37 | if "pyodide" in sys.modules: 38 | import pyodide.http 39 | 40 | response = await pyodide.http.pyfetch(url) 41 | 42 | if type == "json": 43 | # .json() parses the response as JSON and converts to dictionary. 44 | data = await response.json() 45 | elif type == "string": 46 | # .string() returns the response as a string. 47 | data = await response.string() 48 | elif type == "bytes": 49 | # .bytes() returns the response as a byte object. 50 | data = await response.bytes() 51 | 52 | return HttpResponse(response.status, data) 53 | 54 | else: 55 | import urllib.request 56 | 57 | response = urllib.request.urlopen(url) 58 | if type == "json": 59 | data = json.loads(response.read().decode("utf-8")) 60 | elif type == "string": 61 | data = response.read().decode("utf-8") 62 | elif type == "bytes": 63 | data = response.read() 64 | 65 | return HttpResponse(response.status, data) 66 | -------------------------------------------------------------------------------- /examples/python/file_download/about.txt: -------------------------------------------------------------------------------- 1 | File download 2 | -------------------------------------------------------------------------------- /examples/python/file_download/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | from datetime import date 4 | from pathlib import Path 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | from shiny import App, render, ui 9 | 10 | 11 | # A card component wrapper. 12 | def ui_card(title, *args): 13 | return ( 14 | ui.div( 15 | {"class": "card mb-4"}, 16 | ui.div(title, class_="card-header"), 17 | ui.div({"class": "card-body"}, *args), 18 | ), 19 | ) 20 | 21 | 22 | app_ui = ui.page_fluid( 23 | ui_card( 24 | "Download a pre-existing file, using its existing name on disk.", 25 | ui.download_button("download1", "Download CSV"), 26 | ), 27 | ui_card( 28 | "Download a PNG that is generated dynamically.", 29 | ui.input_text("title", "Plot title", "Random scatter plot"), 30 | ui.input_slider("num_points", "Number of data points", 1, 100, 50), 31 | ui.download_button("download2", "Download PNG"), 32 | ), 33 | ui_card( 34 | "Download a file with name that is generated dynamically.", 35 | ui.download_button("download3", "Download CSV"), 36 | ), 37 | ) 38 | 39 | 40 | def server(input, output, session): 41 | @render.download() 42 | def download1(): 43 | # This is the simplest case. The implementation simply returns the path to a 44 | # file on disk. 45 | path = Path(__file__).parent / "mtcars.csv" 46 | return str(path) 47 | 48 | @render.download(filename="image.png") 49 | def download2(): 50 | # Another way to implement a file download is by yielding bytes; either all at 51 | # once, like in this case, or by yielding multiple times. When using this 52 | # approach, you should pass a filename argument to @render.download, which 53 | # determines what the browser will name the downloaded file. 54 | x = np.random.uniform(size=input.num_points()) 55 | y = np.random.uniform(size=input.num_points()) 56 | plt.figure() 57 | plt.scatter(x, y) 58 | plt.title(input.title()) 59 | with io.BytesIO() as buf: 60 | plt.savefig(buf, format="png") 61 | yield buf.getvalue() 62 | 63 | @render.download( 64 | filename=lambda: f"data-{date.today().isoformat()}-{np.random.randint(100,999)}.csv" 65 | ) 66 | async def download3(): 67 | # This version uses a function to generate the filename. It also yields data 68 | # multiple times. 69 | await asyncio.sleep(0.25) 70 | yield "one,two,three\n" 71 | yield "新,1,2\n" 72 | yield "型,4,5\n" 73 | 74 | 75 | app = App(app_ui, server) 76 | -------------------------------------------------------------------------------- /examples/python/file_download/mtcars.csv: -------------------------------------------------------------------------------- 1 | "mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" 2 | 21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | 21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | 22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | 21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | 18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | 18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | 14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | 24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | 22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | 19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | 17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | 16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | 17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | 15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | 10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | 10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | 14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | 32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | 30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | 33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | 21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | 15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | 15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | 13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | 19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | 27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | 26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | 30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | 15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | 19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | 15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | 21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/python/file_download_core/about.txt: -------------------------------------------------------------------------------- 1 | File download 2 | -------------------------------------------------------------------------------- /examples/python/file_download_core/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import io 3 | from datetime import date 4 | from pathlib import Path 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | from shiny import App, render, ui 9 | 10 | 11 | # A card component wrapper. 12 | def ui_card(title, *args): 13 | return ( 14 | ui.div( 15 | {"class": "card mb-4"}, 16 | ui.div(title, class_="card-header"), 17 | ui.div({"class": "card-body"}, *args), 18 | ), 19 | ) 20 | 21 | 22 | app_ui = ui.page_fluid( 23 | ui_card( 24 | "Download a pre-existing file, using its existing name on disk.", 25 | ui.download_button("download1", "Download CSV"), 26 | ), 27 | ui_card( 28 | "Download a PNG that is generated dynamically.", 29 | ui.input_text("title", "Plot title", "Random scatter plot"), 30 | ui.input_slider("num_points", "Number of data points", 1, 100, 50), 31 | ui.download_button("download2", "Download PNG"), 32 | ), 33 | ui_card( 34 | "Download a file with name that is generated dynamically.", 35 | ui.download_button("download3", "Download CSV"), 36 | ), 37 | ) 38 | 39 | 40 | def server(input, output, session): 41 | @render.download() 42 | def download1(): 43 | # This is the simplest case. The implementation simply returns the path to a 44 | # file on disk. 45 | path = Path(__file__).parent / "mtcars.csv" 46 | return str(path) 47 | 48 | @render.download(filename="image.png") 49 | def download2(): 50 | # Another way to implement a file download is by yielding bytes; either all at 51 | # once, like in this case, or by yielding multiple times. When using this 52 | # approach, you should pass a filename argument to @render.download, which 53 | # determines what the browser will name the downloaded file. 54 | x = np.random.uniform(size=input.num_points()) 55 | y = np.random.uniform(size=input.num_points()) 56 | plt.figure() 57 | plt.scatter(x, y) 58 | plt.title(input.title()) 59 | with io.BytesIO() as buf: 60 | plt.savefig(buf, format="png") 61 | yield buf.getvalue() 62 | 63 | @render.download( 64 | filename=lambda: f"data-{date.today().isoformat()}-{np.random.randint(100,999)}.csv" 65 | ) 66 | async def download3(): 67 | # This version uses a function to generate the filename. It also yields data 68 | # multiple times. 69 | await asyncio.sleep(0.25) 70 | yield "one,two,three\n" 71 | yield "新,1,2\n" 72 | yield "型,4,5\n" 73 | 74 | 75 | app = App(app_ui, server) 76 | -------------------------------------------------------------------------------- /examples/python/file_download_core/mtcars.csv: -------------------------------------------------------------------------------- 1 | "mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" 2 | 21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | 21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | 22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | 21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | 18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | 18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | 14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | 24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | 22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | 19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | 17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | 16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | 17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | 15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | 10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | 10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | 14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | 32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | 30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | 33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | 21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | 15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | 15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | 13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | 19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | 27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | 26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | 30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | 15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | 19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | 15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | 21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/python/file_upload/about.txt: -------------------------------------------------------------------------------- 1 | File upload 2 | -------------------------------------------------------------------------------- /examples/python/file_upload/app.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | from math import ceil 3 | from typing import List 4 | 5 | from shiny.express import input, render, ui 6 | 7 | MAX_SIZE = 50000 8 | ui.input_file("file1", "Choose a file to upload:", multiple=True) 9 | ui.input_radio_buttons("type", "Type:", ["Text", "Binary"]) 10 | 11 | 12 | def format_hexdump(data: bytes) -> str: 13 | hex_vals = ["{:02x}".format(b) for b in data] 14 | hex_vals = group_into_blocks(hex_vals, 16) 15 | hex_vals = [" ".join(row) for row in hex_vals] 16 | hex_vals = "\n".join(hex_vals) 17 | return hex_vals 18 | 19 | 20 | def group_into_blocks(x: List[str], blocksize: int): 21 | """ 22 | Given a list, return a list of lists, where the inner lists each have `blocksize` 23 | elements. 24 | """ 25 | return [ 26 | x[i * blocksize : (i + 1) * blocksize] for i in range(ceil(len(x) / blocksize)) 27 | ] 28 | 29 | 30 | @render.code 31 | def file_content(): 32 | file_infos = input.file1() 33 | if not file_infos: 34 | return 35 | 36 | # file_infos is a list of dicts; each dict represents one file. Example: 37 | # [ 38 | # { 39 | # 'name': 'data.csv', 40 | # 'size': 2601, 41 | # 'type': 'text/csv', 42 | # 'datapath': '/tmp/fileupload-1wnx_7c2/tmpga4x9mps/0.csv' 43 | # } 44 | # ] 45 | out_str = "" 46 | for file_info in file_infos: 47 | out_str += ( 48 | "=" * 47 49 | + "\n" 50 | + file_info["name"] 51 | + "\nMIME type: " 52 | + str(mimetypes.guess_type(file_info["name"])[0]) 53 | ) 54 | if file_info["size"] > MAX_SIZE: 55 | out_str += f"\nTruncating at {MAX_SIZE} bytes." 56 | 57 | out_str += "\n" + "=" * 47 + "\n" 58 | 59 | if input.type() == "Text": 60 | with open(file_info["datapath"], "r") as f: 61 | out_str += f.read(MAX_SIZE) 62 | else: 63 | with open(file_info["datapath"], "rb") as f: 64 | data = f.read(MAX_SIZE) 65 | out_str += format_hexdump(data) 66 | 67 | return out_str 68 | -------------------------------------------------------------------------------- /examples/python/hello_world/about.txt: -------------------------------------------------------------------------------- 1 | Hello, World 2 | A non-application example 3 | -------------------------------------------------------------------------------- /examples/python/hello_world/hello.py: -------------------------------------------------------------------------------- 1 | # Python code that is not part of a Shiny app can also be used for examples 2 | # in Shinylive. 3 | # You can run this line-by-line by pressing cmd- or ctrl-Enter, or you can 4 | # select a block of text and run it with cmd-/ctrl-Enter. 5 | def hello(): 6 | print("Hello, world!") 7 | 8 | 9 | hello() 10 | -------------------------------------------------------------------------------- /examples/python/input_checkbox/about.txt: -------------------------------------------------------------------------------- 1 | Checkbox input 2 | -------------------------------------------------------------------------------- /examples/python/input_checkbox/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_checkbox("x", "Checkbox input"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f"x: {input.x()}" 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_checkbox_group/about.txt: -------------------------------------------------------------------------------- 1 | Checkbox group input 2 | -------------------------------------------------------------------------------- /examples/python/input_checkbox_group/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_checkbox_group( 5 | "x", "Checkbox group input", {"a": "Choice A", "b": "Choice B"} 6 | ), 7 | ui.output_code("txt"), 8 | ) 9 | 10 | 11 | def server(input, output, session): 12 | @render.code 13 | def txt(): 14 | return f"x: {input.x()}" 15 | 16 | 17 | app = App(app_ui, server, debug=True) 18 | -------------------------------------------------------------------------------- /examples/python/input_date/about.txt: -------------------------------------------------------------------------------- 1 | Date input 2 | -------------------------------------------------------------------------------- /examples/python/input_date/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_date("x", "Date input"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f"x: {input.x()}" 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_date_range/about.txt: -------------------------------------------------------------------------------- 1 | Date range input 2 | -------------------------------------------------------------------------------- /examples/python/input_date_range/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_date_range("x", "Date range input"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f"x: {input.x()}" 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_numeric/about.txt: -------------------------------------------------------------------------------- 1 | Numeric input 2 | -------------------------------------------------------------------------------- /examples/python/input_numeric/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_numeric("x", "Number input", value=100), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f"x: {input.x()}" 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_password/about.txt: -------------------------------------------------------------------------------- 1 | Password input 2 | -------------------------------------------------------------------------------- /examples/python/input_password/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_password("x", "Password input", placeholder="Enter password"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f'x: "{input.x()}"' 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_radio/about.txt: -------------------------------------------------------------------------------- 1 | Radio buttons input 2 | -------------------------------------------------------------------------------- /examples/python/input_radio/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_radio_buttons( 5 | "x", "Radio buttons input", {"a": "Choice A", "b": "Choice B"} 6 | ), 7 | ui.output_code("txt"), 8 | ) 9 | 10 | 11 | def server(input, output, session): 12 | @render.code 13 | def txt(): 14 | return f'x: "{input.x()}"' 15 | 16 | 17 | app = App(app_ui, server, debug=True) 18 | -------------------------------------------------------------------------------- /examples/python/input_select/about.txt: -------------------------------------------------------------------------------- 1 | Select input 2 | -------------------------------------------------------------------------------- /examples/python/input_select/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_select("x", "Select input", {"a": "Choice A", "b": "Choice B"}), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f'x: "{input.x()}"' 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_slider/about.txt: -------------------------------------------------------------------------------- 1 | Slider input 2 | -------------------------------------------------------------------------------- /examples/python/input_slider/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_slider("x", "Slider input", min=0, max=20, value=10), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f"x: {input.x()}" 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_switch/about.txt: -------------------------------------------------------------------------------- 1 | Switch input 2 | -------------------------------------------------------------------------------- /examples/python/input_switch/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_switch("x", "Switch input"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f"x: {input.x()}" 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_text/about.txt: -------------------------------------------------------------------------------- 1 | Text input 2 | -------------------------------------------------------------------------------- /examples/python/input_text/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_text("x", "Text input", placeholder="Enter text"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f'x: "{input.x()}"' 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_text_area/about.txt: -------------------------------------------------------------------------------- 1 | Text area input 2 | -------------------------------------------------------------------------------- /examples/python/input_text_area/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_text_area("x", "Text area input", placeholder="Enter text"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f'x: "{input.x()}"' 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/input_update/about.txt: -------------------------------------------------------------------------------- 1 | Dynamically updating inputs 2 | -------------------------------------------------------------------------------- /examples/python/input_update/app.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from shiny import reactive 4 | from shiny.express import input, ui 5 | 6 | ui.h1("Updating inputs") 7 | 8 | ui.markdown( 9 | """ 10 | Each Shiny input has an `update_*` function which can be used to update that input. 11 | Most options can be changed including the value, style, and input label, please see 12 | [the docs](https://shiny.posit.co/py/api/ui.update_sidebar.html) for more examples. 13 | """ 14 | ) 15 | 16 | ui.input_slider("slider", "Slider", 0, 100, 50, width="50%") 17 | ui.input_action_button( 18 | "to_20", "Set slider to 20", class_="btn btn-primary", width="25%" 19 | ) 20 | ui.input_action_button( 21 | "to_60", "Set slider to 60", class_="btn btn-primary", width="25%" 22 | ) 23 | 24 | 25 | @reactive.effect 26 | @reactive.event(input.to_20) 27 | def set_to_20(): 28 | ui.update_slider("slider", value=20) 29 | 30 | 31 | @reactive.effect 32 | @reactive.event(input.to_60) 33 | def set_to_60(): 34 | ui.update_slider("slider", value=60) 35 | -------------------------------------------------------------------------------- /examples/python/insert_ui/about.txt: -------------------------------------------------------------------------------- 1 | Dynamically inserting UI 2 | -------------------------------------------------------------------------------- /examples/python/insert_ui/app.py: -------------------------------------------------------------------------------- 1 | from shiny import reactive 2 | from shiny.express import input, render, ui 3 | 4 | ui.h2("Dynamic UI") 5 | with ui.div(id="main-content"): 6 | ui.input_action_button("btn", "Trigger insert/remove ui") 7 | 8 | @render.ui 9 | def dyn_ui(): 10 | return ui.input_slider( 11 | "n1", "This slider is rendered via @render.ui", 0, 100, 20 12 | ) 13 | 14 | # Another way of adding dynamic content is with ui.insert_ui() and ui.remove_ui(). 15 | # The insertion is imperative, so, compared to @render.ui, more care is needed to 16 | # make sure you don't add multiple copies of the content. 17 | @reactive.effect 18 | def _(): 19 | btn = input.btn() 20 | if btn % 2 == 1: 21 | slider = ui.input_slider( 22 | "n2", "This slider is inserted with ui.insert_ui()", 0, 100, 20 23 | ) 24 | ui.insert_ui( 25 | ui.div({"id": "inserted-slider"}, slider), 26 | selector="#main-content", 27 | where="beforeEnd", 28 | ) 29 | elif btn > 0: 30 | ui.remove_ui("#inserted-slider") 31 | -------------------------------------------------------------------------------- /examples/python/ipyleaflet/about.txt: -------------------------------------------------------------------------------- 1 | Map 2 | Interactive map with ipyleaflet 3 | -------------------------------------------------------------------------------- /examples/python/ipyleaflet/app.py: -------------------------------------------------------------------------------- 1 | from shiny import reactive 2 | from shiny.express import input, ui 3 | from shinywidgets import render_widget 4 | import ipyleaflet as ipyl 5 | 6 | city_centers = { 7 | "London": (51.5074, 0.1278), 8 | "Paris": (48.8566, 2.3522), 9 | "New York": (40.7128, -74.0060), 10 | } 11 | ui.input_select("center", "Center", choices=list(city_centers.keys())) 12 | 13 | 14 | @render_widget 15 | def map(): 16 | return ipyl.Map(zoom=4) 17 | 18 | 19 | @reactive.effect 20 | def _(): 21 | map.widget.center = city_centers[input.center()] 22 | -------------------------------------------------------------------------------- /examples/python/layout_sidebar/about.txt: -------------------------------------------------------------------------------- 1 | Sidebar 2 | -------------------------------------------------------------------------------- /examples/python/layout_sidebar/app.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from shiny import App, render, ui 4 | 5 | # Note that if the window is narrow, then the sidebar will be shown above the 6 | # main content, instead of being on the left. 7 | 8 | app_ui = ui.page_fluid( 9 | ui.layout_sidebar( 10 | ui.panel_sidebar(ui.input_slider("n", "N", 0, 100, 20)), 11 | ui.panel_main(ui.output_plot("plot")), 12 | ), 13 | ) 14 | 15 | 16 | def server(input, output, session): 17 | @render.plot(alt="A histogram") 18 | def plot() -> object: 19 | np.random.seed(19680801) 20 | x = 100 + 15 * np.random.randn(437) 21 | 22 | fig, ax = plt.subplots() 23 | ax.hist(x, input.n(), density=True) 24 | return fig 25 | 26 | 27 | app = App(app_ui, server) 28 | -------------------------------------------------------------------------------- /examples/python/layout_two_column/about.txt: -------------------------------------------------------------------------------- 1 | Two columns 2 | -------------------------------------------------------------------------------- /examples/python/layout_two_column/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.tags.style( 5 | """ 6 | .app-col { 7 | border: 1px solid black; 8 | border-radius: 5px; 9 | background-color: #eee; 10 | padding: 8px; 11 | margin-top: 5px; 12 | margin-bottom: 5px; 13 | } 14 | """ 15 | ), 16 | ui.h2({"style": "text-align: center;"}, "App Title"), 17 | ui.row( 18 | ui.column( 19 | 12, 20 | ui.div( 21 | {"class": "app-col"}, 22 | ui.p( 23 | """ 24 | This is a column that spans the entire width of the page. 25 | """, 26 | ), 27 | ui.p( 28 | """ 29 | Here's some more text in another paragraph. 30 | """, 31 | ), 32 | ), 33 | ) 34 | ), 35 | ui.row( 36 | ui.column( 37 | 6, 38 | ui.div( 39 | {"class": "app-col"}, 40 | """ 41 | Here's some text in a column. Note that if the page is very 42 | narrow, the two columns will be be stacked on top of each other 43 | instead of being side-by-side. 44 | """, 45 | ), 46 | ), 47 | ui.column( 48 | 6, 49 | ui.div( 50 | {"class": "app-col"}, 51 | """ 52 | Here's some text in another column. 53 | """, 54 | ), 55 | ), 56 | ), 57 | ) 58 | 59 | 60 | def server(input, output, session): 61 | pass 62 | 63 | 64 | app = App(app_ui, server) 65 | -------------------------------------------------------------------------------- /examples/python/modules/about.txt: -------------------------------------------------------------------------------- 1 | Modules 2 | Reusable Shiny components 3 | -------------------------------------------------------------------------------- /examples/python/modules/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, module, reactive, render, ui 2 | 3 | 4 | # ============================================================ 5 | # Counter module 6 | # ============================================================ 7 | @module.ui 8 | def counter_ui(label: str = "Increment counter") -> ui.TagChild: 9 | return ui.card( 10 | ui.h2("This is " + label), 11 | ui.input_action_button(id="button", label=label), 12 | ui.output_code(id="out"), 13 | ) 14 | 15 | 16 | @module.server 17 | def counter_server(input, output, session, starting_value: int = 0): 18 | count: reactive.value[int] = reactive.value(starting_value) 19 | 20 | @reactive.effect 21 | @reactive.event(input.button) 22 | def _(): 23 | count.set(count() + 1) 24 | 25 | @render.code 26 | def out() -> str: 27 | return f"Click count is {count()}" 28 | 29 | 30 | # ============================================================================= 31 | # App that uses module 32 | # ============================================================================= 33 | app_ui = ui.page_fluid( 34 | counter_ui("counter1", "Counter 1"), 35 | counter_ui("counter2", "Counter 2"), 36 | ) 37 | 38 | 39 | def server(input, output, session): 40 | counter_server("counter1") 41 | counter_server("counter2") 42 | 43 | 44 | app = App(app_ui, server) 45 | -------------------------------------------------------------------------------- /examples/python/multiple_source_files/about.txt: -------------------------------------------------------------------------------- 1 | Multiple source files 2 | -------------------------------------------------------------------------------- /examples/python/multiple_source_files/app.py: -------------------------------------------------------------------------------- 1 | from shiny.express import input, render, ui 2 | from utils import square 3 | 4 | ui.input_slider("n", "N", 0, 100, 20) 5 | 6 | 7 | @render.code 8 | def txt(): 9 | val = square(input.n()) 10 | return f"{input.n()} squared is {val}" 11 | -------------------------------------------------------------------------------- /examples/python/multiple_source_files/utils.py: -------------------------------------------------------------------------------- 1 | def square(n): 2 | return n * n 3 | -------------------------------------------------------------------------------- /examples/python/orbit/about.txt: -------------------------------------------------------------------------------- 1 | Orbit simulation 2 | Earth-Moon gravity 3 | -------------------------------------------------------------------------------- /examples/python/orbit/body.py: -------------------------------------------------------------------------------- 1 | """Body Shiny module 2 | 3 | A Shiny module that represents a body (i.e. planet/moon) in the simulation. This allows 4 | us to have multiple bodies in the simulation, each sharing similar UI and server logic, 5 | without having to repeat the code. 6 | 7 | Learn more about Shiny modules at: https://shiny.posit.co/py/docs/workflow-modules.html 8 | """ 9 | 10 | import astropy.units as u 11 | import numpy as np 12 | from shiny import module, reactive, ui 13 | from simulation import Body, spherical_to_cartesian 14 | 15 | 16 | @module.ui 17 | def body_ui(enable, mass, speed, theta, phi): 18 | return ui.TagList( 19 | ui.input_checkbox("enable", "Enable", enable), 20 | ui.panel_conditional( 21 | "input.enable", 22 | ui.input_numeric( 23 | "mass", 24 | "Mass (10^22 kg)", 25 | mass, 26 | ), 27 | ui.input_slider( 28 | "speed", 29 | "Speed (km/s)", 30 | 0, 31 | 1, 32 | value=speed, 33 | step=0.001, 34 | ), 35 | ui.input_slider("theta", "Angle (𝜃)", 0, 360, value=theta), 36 | ui.input_slider("phi", "𝜙", 0, 180, value=phi), 37 | ), 38 | ) 39 | 40 | 41 | @module.server 42 | def body_server(input, output, session, label, start_vec): 43 | @reactive.calc 44 | def body_result(): 45 | if not input.enable(): 46 | return None 47 | 48 | v = spherical_to_cartesian(input.theta(), input.phi(), input.speed()) 49 | 50 | return Body( 51 | mass=input.mass() * 1e22 * u.kg, 52 | x_vec=np.array(start_vec) * u.km, 53 | v_vec=np.array(v) * u.km / u.s, 54 | name=label, 55 | ) 56 | 57 | return body_result 58 | -------------------------------------------------------------------------------- /examples/python/orbit/requirements.txt: -------------------------------------------------------------------------------- 1 | astropy 2 | -------------------------------------------------------------------------------- /examples/python/orbit/www/coords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/shinylive/8acb41b0d4090308551e2f30dccb68fa6c9071a9/examples/python/orbit/www/coords.png -------------------------------------------------------------------------------- /examples/python/output_code/about.txt: -------------------------------------------------------------------------------- 1 | Code output 2 | -------------------------------------------------------------------------------- /examples/python/output_code/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_text("x", "Text input", placeholder="Enter text"), 5 | ui.output_code("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.code 11 | def txt(): 12 | return f'x: "{input.x()}"' 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/output_data_frame_grid/about.txt: -------------------------------------------------------------------------------- 1 | Data Frame/Grid 2 | -------------------------------------------------------------------------------- /examples/python/output_data_frame_grid/app.py: -------------------------------------------------------------------------------- 1 | import palmerpenguins 2 | from shiny import App, render, ui 3 | 4 | penguins = palmerpenguins.load_penguins() 5 | # Slim down the data frame to a few representative columns 6 | penguins = penguins.loc[ 7 | penguins["body_mass_g"].notnull(), 8 | ["species", "island", "body_mass_g", "year"], 9 | ] 10 | 11 | app_ui = ui.page_fluid( 12 | ui.input_select( 13 | "selection_mode", 14 | "Selection mode", 15 | {"none": "(None)", "single": "Single", "multiple": "Multiple"}, 16 | selected="multiple", 17 | ), 18 | ui.input_switch("gridstyle", "Grid", True), 19 | ui.input_switch("fullwidth", "Take full width", True), 20 | ui.input_switch("fixedheight", "Fixed height", True), 21 | ui.input_switch("filters", "Filters", True), 22 | ui.output_data_frame("grid"), 23 | ui.panel_fixed( 24 | ui.output_code("detail"), 25 | right="10px", 26 | bottom="10px", 27 | ), 28 | class_="p-3", 29 | ) 30 | 31 | 32 | def server(input, output, session): 33 | @render.data_frame 34 | def grid(): 35 | height = 350 if input.fixedheight() else None 36 | width = "100%" if input.fullwidth() else "fit-content" 37 | if input.gridstyle(): 38 | return render.DataGrid( 39 | penguins, 40 | row_selection_mode=input.selection_mode(), 41 | height=height, 42 | width=width, 43 | filters=input.filters(), 44 | ) 45 | else: 46 | return render.DataTable( 47 | penguins, 48 | row_selection_mode=input.selection_mode(), 49 | height=height, 50 | width=width, 51 | filters=input.filters(), 52 | ) 53 | 54 | @render.code 55 | def detail(): 56 | if ( 57 | input.grid_selected_rows() is not None 58 | and len(input.grid_selected_rows()) > 0 59 | ): 60 | # "split", "records", "index", "columns", "values", "table" 61 | return penguins.iloc[list(input.grid_selected_rows())] 62 | 63 | 64 | app = App(app_ui, server) 65 | -------------------------------------------------------------------------------- /examples/python/output_plot/about.txt: -------------------------------------------------------------------------------- 1 | Plot output 2 | -------------------------------------------------------------------------------- /examples/python/output_plot/app.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from shiny import App, render, ui 4 | 5 | app_ui = ui.page_fluid( 6 | ui.input_slider("n", "Number of bins", 0, 100, 20), 7 | ui.output_plot("plot"), 8 | ) 9 | 10 | 11 | def server(input, output, session): 12 | @render.plot(alt="A histogram") 13 | def plot(): 14 | np.random.seed(19680801) 15 | x = 100 + 15 * np.random.randn(437) 16 | 17 | fig, ax = plt.subplots() 18 | ax.hist(x, input.n(), density=True) 19 | return fig 20 | 21 | 22 | app = App(app_ui, server, debug=True) 23 | -------------------------------------------------------------------------------- /examples/python/output_table/about.txt: -------------------------------------------------------------------------------- 1 | Table output 2 | -------------------------------------------------------------------------------- /examples/python/output_table/app.py: -------------------------------------------------------------------------------- 1 | import palmerpenguins 2 | from shiny import App, render, ui 3 | 4 | penguins = palmerpenguins.load_penguins() 5 | 6 | numeric_cols = [ 7 | "bill_length_mm", 8 | "bill_depth_mm", 9 | "flipper_length_mm", 10 | "body_mass_g", 11 | ] 12 | 13 | app_ui = ui.page_fluid( 14 | ui.input_checkbox("highlight", "Highlight min/max values"), 15 | ui.output_table("result"), 16 | # Legend 17 | ui.panel_conditional( 18 | "input.highlight", 19 | ui.panel_absolute( 20 | ui.span("minimum", style="background-color: silver;"), 21 | ui.span("maximum", style="background-color: yellow;"), 22 | top="6px", 23 | right="6px", 24 | class_="p-1 bg-light border", 25 | ), 26 | ), 27 | class_="p-3", 28 | ) 29 | 30 | 31 | def server(input, output, session): 32 | @render.table 33 | def result(): 34 | if not input.highlight(): 35 | # If we're not highlighting values, we can simply 36 | # return the pandas data frame as-is; @render.table 37 | # will call .to_html() on it. 38 | return penguins 39 | else: 40 | # We need to use the pandas Styler API. The default 41 | # formatting options for Styler are not the same as 42 | # DataFrame.to_html(), so we set a few options to 43 | # make them match. 44 | return ( 45 | penguins.style.set_table_attributes( 46 | 'class="dataframe shiny-table table w-auto"' 47 | ) 48 | .hide(axis="index") 49 | .format( 50 | { 51 | "bill_length_mm": "{0:0.1f}", 52 | "bill_depth_mm": "{0:0.1f}", 53 | "flipper_length_mm": "{0:0.0f}", 54 | "body_mass_g": "{0:0.0f}", 55 | } 56 | ) 57 | .set_table_styles( 58 | [ 59 | dict(selector="th", props=[("text-align", "right")]), 60 | dict( 61 | selector="tr>td", 62 | props=[ 63 | ("padding-top", "0.1rem"), 64 | ("padding-bottom", "0.1rem"), 65 | ], 66 | ), 67 | ] 68 | ) 69 | .highlight_min(color="silver", subset=numeric_cols) 70 | .highlight_max(color="yellow", subset=numeric_cols) 71 | ) 72 | 73 | 74 | app = App(app_ui, server) 75 | -------------------------------------------------------------------------------- /examples/python/output_text/about.txt: -------------------------------------------------------------------------------- 1 | Text output 2 | -------------------------------------------------------------------------------- /examples/python/output_text/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.input_text("x", "Text input", placeholder="Enter text"), 5 | ui.output_text("txt"), 6 | ) 7 | 8 | 9 | def server(input, output, session): 10 | @render.text 11 | def txt(): 12 | return f'x: "{input.x()}"' 13 | 14 | 15 | app = App(app_ui, server, debug=True) 16 | -------------------------------------------------------------------------------- /examples/python/output_ui/about.txt: -------------------------------------------------------------------------------- 1 | UI output 2 | -------------------------------------------------------------------------------- /examples/python/output_ui/app.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from shiny.express import expressify, input, output, render, ui 4 | 5 | ui.input_slider("card_n", "Number of cards", value=3, min=1, max=5) 6 | 7 | 8 | @expressify 9 | def custom_card(id): 10 | id = id + 1 11 | with ui.card(): 12 | f"Card {id}" 13 | 14 | # Specifying the ID like this lets us include a renderer in the iterator 15 | # without causing ID conflicts. 16 | @output(id=f"hist_{id }") 17 | @render.plot(alt="A histogram") 18 | def histogram(): 19 | np.random.seed(19680801) 20 | x = 100 + 15 * np.random.randn(437) 21 | plt.hist(x, 20, density=True) 22 | 23 | 24 | @render.express 25 | def cards(): 26 | with ui.layout_columns(): 27 | for i in range(input.card_n()): 28 | custom_card(i) 29 | -------------------------------------------------------------------------------- /examples/python/plot_interact_basic/about.txt: -------------------------------------------------------------------------------- 1 | Basic plot interaction 2 | -------------------------------------------------------------------------------- /examples/python/plot_interact_basic/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import matplotlib.pyplot as plt 5 | import pandas as pd 6 | from shiny import App, render, ui 7 | 8 | mtcars = pd.read_csv(Path(__file__).parent / "mtcars.csv") 9 | mtcars.drop(["disp", "hp", "drat", "qsec", "vs", "gear", "carb"], axis=1, inplace=True) 10 | 11 | 12 | app_ui = ui.page_fluid( 13 | ui.head_content( 14 | ui.tags.style( 15 | """ 16 | /* Smaller font for preformatted text */ 17 | pre, table.table { 18 | font-size: smaller; 19 | } 20 | 21 | pre, table.table { 22 | font-size: smaller; 23 | } 24 | """ 25 | ) 26 | ), 27 | ui.row( 28 | ui.column( 29 | 4, 30 | ui.panel_well( 31 | ui.input_radio_buttons( 32 | "plot_type", "Plot type", ["matplotlib", "plotnine"] 33 | ) 34 | ), 35 | ), 36 | ui.column( 37 | 8, 38 | ui.output_plot("plot1", click=True, dblclick=True, hover=True, brush=True), 39 | ), 40 | ), 41 | ui.row( 42 | ui.column(3, ui.output_code("click_info")), 43 | ui.column(3, ui.output_code("dblclick_info")), 44 | ui.column(3, ui.output_code("hover_info")), 45 | ui.column(3, ui.output_code("brush_info")), 46 | ), 47 | ) 48 | 49 | 50 | def server(input, output, session): 51 | @render.plot(alt="A scatterplot") 52 | def plot1(): 53 | if input.plot_type() == "matplotlib": 54 | fig, ax = plt.subplots() 55 | plt.title("Good old mtcars") 56 | ax.scatter(mtcars["wt"], mtcars["mpg"]) 57 | return fig 58 | 59 | elif input.plot_type() == "plotnine": 60 | from plotnine import aes, geom_point, ggplot, ggtitle 61 | 62 | p = ( 63 | ggplot(mtcars, aes("wt", "mpg")) 64 | + geom_point() 65 | + ggtitle("Good old mtcars") 66 | ) 67 | 68 | return p 69 | 70 | @render.code() 71 | def click_info(): 72 | return "click:\n" + json.dumps(input.plot1_click(), indent=2) 73 | 74 | @render.code() 75 | def dblclick_info(): 76 | return "dblclick:\n" + json.dumps(input.plot1_dblclick(), indent=2) 77 | 78 | @render.code() 79 | def hover_info(): 80 | return "hover:\n" + json.dumps(input.plot1_hover(), indent=2) 81 | 82 | @render.code() 83 | def brush_info(): 84 | return "brush:\n" + json.dumps(input.plot1_brush(), indent=2) 85 | 86 | 87 | app = App(app_ui, server, debug=True) 88 | -------------------------------------------------------------------------------- /examples/python/plot_interact_basic/mtcars.csv: -------------------------------------------------------------------------------- 1 | "model","mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" 2 | "Mazda RX4",21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | "Mazda RX4 Wag",21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | "Datsun 710",22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | "Hornet 4 Drive",21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | "Hornet Sportabout",18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | "Valiant",18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | "Duster 360",14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | "Merc 240D",24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | "Merc 230",22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | "Merc 280",19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | "Merc 280C",17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | "Merc 450SE",16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | "Merc 450SL",17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | "Merc 450SLC",15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | "Cadillac Fleetwood",10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | "Lincoln Continental",10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | "Chrysler Imperial",14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | "Fiat 128",32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | "Honda Civic",30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | "Toyota Corolla",33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | "Toyota Corona",21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | "Dodge Challenger",15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | "AMC Javelin",15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | "Camaro Z28",13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | "Pontiac Firebird",19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | "Fiat X1-9",27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | "Porsche 914-2",26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | "Lotus Europa",30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | "Ford Pantera L",15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | "Ferrari Dino",19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | "Maserati Bora",15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | "Volvo 142E",21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/python/plot_interact_exclude/about.txt: -------------------------------------------------------------------------------- 1 | Interactively excluding data 2 | -------------------------------------------------------------------------------- /examples/python/plot_interact_exclude/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import statsmodels.api as sm 6 | from plotnine import aes, geom_point, geom_smooth, ggplot 7 | from shiny import App, reactive, render, ui 8 | from shiny.plotutils import brushed_points, near_points 9 | 10 | mtcars = pd.read_csv(Path(__file__).parent / "mtcars.csv") 11 | mtcars.drop(["disp", "hp", "drat", "qsec", "vs", "gear", "carb"], axis=1, inplace=True) 12 | 13 | 14 | app_ui = ui.page_fluid( 15 | ui.head_content( 16 | ui.tags.style( 17 | """ 18 | pre, table.table { 19 | font-size: smaller; 20 | } 21 | """ 22 | ) 23 | ), 24 | ui.row( 25 | ui.column(2), 26 | ui.column( 27 | 8, 28 | ui.output_plot("plot1", click=True, brush=True), 29 | ui.div( 30 | {"style": "text-align: center"}, 31 | ui.input_action_button("exclude_toggle", "Toggle brushed points"), 32 | ui.input_action_button("exclude_reset", "Reset"), 33 | ), 34 | ), 35 | ), 36 | ui.row( 37 | ui.column(12, {"style": "margin-top: 15px;"}, ui.output_code("model")), 38 | ), 39 | ) 40 | 41 | 42 | def server(input, output, session): 43 | keep_rows = reactive.value([True] * len(mtcars)) 44 | 45 | @reactive.calc 46 | def data_with_keep(): 47 | df = mtcars.copy() 48 | df["keep"] = keep_rows() 49 | return df 50 | 51 | @reactive.effect 52 | @reactive.event(input.plot1_click) 53 | def _(): 54 | res = near_points(mtcars, input.plot1_click(), all_rows=True, max_points=1) 55 | keep_rows.set(list(np.logical_xor(keep_rows(), res.selected_))) 56 | 57 | @reactive.effect 58 | @reactive.event(input.exclude_toggle) 59 | def _(): 60 | res = brushed_points(mtcars, input.plot1_brush(), all_rows=True) 61 | keep_rows.set(list(np.logical_xor(keep_rows(), res.selected_))) 62 | 63 | @reactive.effect 64 | @reactive.event(input.exclude_reset) 65 | def _(): 66 | keep_rows.set([True] * len(mtcars)) 67 | 68 | @render.plot() 69 | def plot1(): 70 | df = data_with_keep() 71 | df_keep = df[df["keep"]] 72 | df_exclude = df[~df["keep"]] 73 | 74 | return ( 75 | ggplot(df_keep, aes("wt", "mpg")) 76 | + geom_point() 77 | + geom_point(data=df_exclude, color="#666", fill="white") 78 | + geom_smooth(method="lm", fullrange=True) 79 | ) 80 | 81 | @render.code() 82 | def model(): 83 | df = data_with_keep() 84 | df_keep = df[df["keep"]] 85 | mod = sm.OLS(df_keep["wt"], df_keep["mpg"]) 86 | res = mod.fit() 87 | return res.summary() 88 | 89 | 90 | app = App(app_ui, server) 91 | -------------------------------------------------------------------------------- /examples/python/plot_interact_exclude/mtcars.csv: -------------------------------------------------------------------------------- 1 | "model","mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" 2 | "Mazda RX4",21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | "Mazda RX4 Wag",21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | "Datsun 710",22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | "Hornet 4 Drive",21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | "Hornet Sportabout",18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | "Valiant",18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | "Duster 360",14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | "Merc 240D",24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | "Merc 230",22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | "Merc 280",19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | "Merc 280C",17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | "Merc 450SE",16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | "Merc 450SL",17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | "Merc 450SLC",15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | "Cadillac Fleetwood",10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | "Lincoln Continental",10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | "Chrysler Imperial",14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | "Fiat 128",32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | "Honda Civic",30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | "Toyota Corolla",33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | "Toyota Corona",21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | "Dodge Challenger",15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | "AMC Javelin",15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | "Camaro Z28",13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | "Pontiac Firebird",19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | "Fiat X1-9",27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | "Porsche 914-2",26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | "Lotus Europa",30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | "Ford Pantera L",15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | "Ferrari Dino",19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | "Maserati Bora",15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | "Volvo 142E",21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/python/plot_interact_select/about.txt: -------------------------------------------------------------------------------- 1 | Selecting data 2 | -------------------------------------------------------------------------------- /examples/python/plot_interact_select/mtcars.csv: -------------------------------------------------------------------------------- 1 | "model","mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" 2 | "Mazda RX4",21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | "Mazda RX4 Wag",21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | "Datsun 710",22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | "Hornet 4 Drive",21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | "Hornet Sportabout",18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | "Valiant",18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | "Duster 360",14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | "Merc 240D",24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | "Merc 230",22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | "Merc 280",19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | "Merc 280C",17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | "Merc 450SE",16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | "Merc 450SL",17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | "Merc 450SLC",15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | "Cadillac Fleetwood",10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | "Lincoln Continental",10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | "Chrysler Imperial",14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | "Fiat 128",32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | "Honda Civic",30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | "Toyota Corolla",33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | "Toyota Corona",21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | "Dodge Challenger",15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | "AMC Javelin",15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | "Camaro Z28",13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | "Pontiac Firebird",19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | "Fiat X1-9",27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | "Porsche 914-2",26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | "Lotus Europa",30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | "Ford Pantera L",15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | "Ferrari Dino",19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | "Maserati Bora",15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | "Volvo 142E",21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/python/plotly/about.txt: -------------------------------------------------------------------------------- 1 | Plotly 2 | Interactive plot with plotly 3 | -------------------------------------------------------------------------------- /examples/python/plotly/app.py: -------------------------------------------------------------------------------- 1 | import plotly.express as px 2 | from shiny.express import input, ui 3 | from shinywidgets import render_plotly 4 | 5 | ui.page_opts(title="Filling layout", fillable=True) 6 | with ui.layout_columns(): 7 | 8 | @render_plotly 9 | def plot1(): 10 | return px.histogram(px.data.tips(), y="tip") 11 | 12 | @render_plotly 13 | def plot2(): 14 | return px.histogram(px.data.tips(), y="total_bill") 15 | -------------------------------------------------------------------------------- /examples/python/plotly/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | plotly 3 | -------------------------------------------------------------------------------- /examples/python/reactive_calc/about.txt: -------------------------------------------------------------------------------- 1 | Reactive calc 2 | -------------------------------------------------------------------------------- /examples/python/reactive_calc/app.py: -------------------------------------------------------------------------------- 1 | # A reactive Calc is used for its return value. It intelligently caches its value, and 2 | # only re-runs after it has been invalidated -- that is, when upstream reactive inputs 3 | # change. 4 | 5 | from shiny import reactive 6 | from shiny.express import input, render, ui 7 | 8 | ui.input_slider("x", "Choose a number", 1, 100, 50) 9 | 10 | 11 | @reactive.calc 12 | def x_times_2(): 13 | val = input.x() * 2 14 | print(f"Running x_times_2(). Result is {val}.") 15 | return val 16 | 17 | 18 | @render.code 19 | def txt1(): 20 | return f'x times 2 is: "{x_times_2()}"' 21 | 22 | 23 | @render.code 24 | def txt2(): 25 | return f'x times 2 is: "{x_times_2()}"' 26 | -------------------------------------------------------------------------------- /examples/python/reactive_effect/about.txt: -------------------------------------------------------------------------------- 1 | Reactive effect 2 | -------------------------------------------------------------------------------- /examples/python/reactive_effect/app.py: -------------------------------------------------------------------------------- 1 | # A reactive Effect is run for its side effects, not for its return value. These 2 | # side effects can include printing messages to the console, writing files to 3 | # disk, or sending messages to a server. 4 | 5 | from shiny import reactive 6 | from shiny.express import input, ui 7 | 8 | ui.input_text("x", "Text input", placeholder="Enter text") 9 | 10 | 11 | @reactive.effect 12 | def _(): 13 | print(f"x has changed to {input.x()}") 14 | -------------------------------------------------------------------------------- /examples/python/reactive_event/about.txt: -------------------------------------------------------------------------------- 1 | Event decorator 2 | -------------------------------------------------------------------------------- /examples/python/reactive_event/app.py: -------------------------------------------------------------------------------- 1 | # A reactive Effect is run for its side effects, not for its return value. These 2 | # side effects can include printing messages to the console, writing files to 3 | # disk, or sending messages to a server. 4 | 5 | from shiny import reactive 6 | from shiny.express import input, render, ui 7 | 8 | ui.input_slider("n", "N", 0, 20, 10) 9 | ui.input_action_button("btn", "Click me") 10 | ui.tags.br() 11 | "The value of the slider when the button was last clicked:" 12 | 13 | 14 | @reactive.effect 15 | @reactive.event(input.btn) 16 | def _(): 17 | print("You clicked the button!") 18 | # You can do other things here, like write data to disk. 19 | 20 | 21 | @render.code 22 | @reactive.event(input.btn) 23 | def txt(): 24 | return f"Last value: {input.n()}" 25 | -------------------------------------------------------------------------------- /examples/python/reactive_value/about.txt: -------------------------------------------------------------------------------- 1 | Reactive value 2 | -------------------------------------------------------------------------------- /examples/python/reactive_value/app.py: -------------------------------------------------------------------------------- 1 | # When a reactive Value's value changes, it will invalidate (and trigger re-execution) 2 | # of any reactive objects (Calcs, Effects, and outputs) that depend on it. Typically, a 3 | # reactive Value will be set with a reactive Effect. The Effect, in turn, may be driven 4 | # by a user input (like a button), or something else, like a timer. 5 | # 6 | # Reactive Values are often used for tracking or accumulating state over time. 7 | # 8 | # In this example, a button press triggers an Effect which adds the current timestamp to 9 | # a reactive Value containing an array of timestamps. Importantly, the Effect sets the 10 | # reactive Value's value, which causes all downstream reactive objects to be 11 | # invalidated. 12 | # 13 | # There is also an output which reads the reactive Value and returns a string. When the 14 | # reactive Value changes, it invalidates this output, causing it to re-execute and 15 | # return a new string. 16 | 17 | import textwrap 18 | from datetime import datetime 19 | 20 | from shiny import reactive 21 | from shiny.express import input, render, ui 22 | 23 | ui.h3("Press the button:") 24 | ui.input_action_button("btn", "Time") 25 | ui.h3("Time between button presses:") 26 | 27 | 28 | # A reactive.value with an array tracking timestamps of all button presses. 29 | all_times = reactive.value([datetime.now().timestamp()]) 30 | 31 | 32 | # This Effect is triggered by pressing the button. It makes a copy of all_times(), 33 | # because we don't want to modify the original, then appends the new timestamp, 34 | # then sets all_times() to the new, longer array. 35 | @reactive.effect 36 | @reactive.event(input.btn) 37 | def _(): 38 | x = all_times().copy() 39 | x.append(datetime.now().timestamp()) 40 | all_times.set(x) 41 | 42 | 43 | # This text output is invalidated when all_times() changes. It calculates the 44 | # differences between each timestamp and returns the array of differences as a 45 | # string. 46 | 47 | 48 | @render.code 49 | def txt(): 50 | x = all_times() 51 | x = [round(j - i, 2) for i, j in zip(x[:-1], x[1:])] 52 | return "\n".join(textwrap.wrap(str(x), width=45)) 53 | -------------------------------------------------------------------------------- /examples/python/read_local_csv_file/about.txt: -------------------------------------------------------------------------------- 1 | Read local CSV 2 | Load a CSV file and display as an HTML table 3 | -------------------------------------------------------------------------------- /examples/python/read_local_csv_file/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pandas 4 | 5 | from shiny import reactive 6 | from shiny.express import render, ui 7 | 8 | 9 | @reactive.calc 10 | def dat(): 11 | infile = Path(__file__).parent / "mtcars.csv" 12 | return pandas.read_csv(infile) 13 | 14 | 15 | with ui.navset_card_underline(): 16 | 17 | with ui.nav_panel("Data frame"): 18 | @render.data_frame 19 | def frame(): 20 | # Give dat() to render.DataGrid to customize the grid 21 | return dat() 22 | 23 | with ui.nav_panel("Table"): 24 | @render.table 25 | def table(): 26 | return dat() 27 | -------------------------------------------------------------------------------- /examples/python/read_local_csv_file/mtcars.csv: -------------------------------------------------------------------------------- 1 | "mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" 2 | 21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | 21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | 22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | 21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | 18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | 18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | 14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | 24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | 22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | 19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | 17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | 16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | 17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | 15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | 10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | 10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | 14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | 32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | 30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | 33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | 21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | 15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | 15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | 13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | 19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | 27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | 26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | 30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | 15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | 19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | 15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | 21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/python/regularization/about.txt: -------------------------------------------------------------------------------- 1 | Regularization 2 | Regularization strength and coefficient estimates 3 | -------------------------------------------------------------------------------- /examples/python/regularization/compare.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet 4 | 5 | # define functions 6 | def sim_data(n=1000): 7 | # Real Variables 8 | A = np.random.normal(0, 1, n) 9 | E = np.random.normal(0, 1, n) 10 | I = np.random.normal(0, 1, n) 11 | O = np.random.normal(0, 1, n) 12 | U = np.random.normal(0, 1, n) 13 | Y = np.random.normal(0, 1, n) 14 | W = np.random.normal(0, 1, n) 15 | 16 | # Unrelated Variables 17 | B = np.random.normal(0, 1, n) 18 | C = np.random.normal(0, 1, n) 19 | D = np.random.normal(0, 1, n) 20 | G = np.random.normal(0, 1, n) 21 | H = np.random.normal(0, 1, n) 22 | J = np.random.normal(0, 1, n) 23 | K = np.random.normal(0, 1, n) 24 | 25 | # coefficients 26 | a = 12.34 27 | e = 8.23 28 | i = 7.83 29 | o = 5.12 30 | u = 3.48 31 | y = 2.97 32 | w = 1.38 33 | 34 | # Outcome 35 | X = ( 36 | 100 37 | + A * a 38 | + E * e 39 | + I * i 40 | + O * o 41 | + U * u 42 | + Y * y 43 | + W * w 44 | + np.random.normal(0, 15, n) 45 | ) 46 | 47 | X = (X - np.mean(X)) / np.std(X) # z-score X 48 | # the other variables already have a mean of 0 and sd of 1 49 | 50 | # Data Frame 51 | df = pd.DataFrame( 52 | { 53 | "A": A, 54 | "E": E, 55 | "I": I, 56 | "O": O, 57 | "U": U, 58 | "B": B, 59 | "C": C, 60 | "D": D, 61 | "G": G, 62 | "H": H, 63 | "J": J, 64 | "K": K, 65 | "Y": Y, 66 | "W": W, 67 | "X": X, 68 | } 69 | ) 70 | return df 71 | 72 | 73 | def compare(df, alpha=1): 74 | feat = ["A", "B", "C", "D", "E", "G", "H", "I", "O", "U", "J", "K", "Y", "W"] 75 | 76 | # linear 77 | lr = LinearRegression() 78 | lr.fit(df[feat], df["X"]) 79 | lr_co = lr.coef_ 80 | 81 | # lasso 82 | lasso = Lasso(alpha=alpha, fit_intercept=True, tol=0.0000001, max_iter=100000) 83 | lasso.fit(df[feat], df["X"]) 84 | lasso_co = lasso.coef_ 85 | 86 | # ridge 87 | ridge = Ridge( 88 | alpha=df.shape[0] * alpha, fit_intercept=True, tol=0.0000001, max_iter=100000 89 | ) 90 | ridge.fit(df[feat], df["X"]) 91 | ridge_co = ridge.coef_ 92 | 93 | conames = feat * 3 94 | coefs = np.concatenate([lr_co, lasso_co, ridge_co]) 95 | 96 | model = np.repeat( 97 | np.array(["Linear", "LASSO", "Ridge"]), 98 | [len(feat), len(feat), len(feat)], 99 | axis=0, 100 | ) 101 | 102 | df = pd.DataFrame({"conames": conames, "coefs": coefs, "model": model}) 103 | 104 | return df 105 | -------------------------------------------------------------------------------- /examples/python/shinyswatch/about.txt: -------------------------------------------------------------------------------- 1 | Shinyswatch 2 | Visual themes 3 | -------------------------------------------------------------------------------- /examples/python/shinyswatch/app.py: -------------------------------------------------------------------------------- 1 | # The shinyswatch package provides themes from https://bootswatch.com/ 2 | 3 | import shinyswatch 4 | from shiny import App, render, ui 5 | 6 | app_ui = ui.page_navbar( 7 | # Available themes: 8 | # cerulean, cosmo, cyborg, darkly, flatly, journal, litera, lumen, lux, 9 | # materia, minty, morph, pulse, quartz, sandstone, simplex, sketchy, slate, 10 | # solar, spacelab, superhero, united, vapor, yeti, zephyr 11 | shinyswatch.theme.superhero(), 12 | ui.nav( 13 | "Navbar 1", 14 | ui.layout_sidebar( 15 | ui.panel_sidebar( 16 | ui.input_file("file", "File input:"), 17 | ui.input_text("txt", "Text input:", "general"), 18 | ui.input_slider("slider", "Slider input:", 1, 100, 30), 19 | ui.tags.h5("Default actionButton:"), 20 | ui.input_action_button("action", "Search"), 21 | ui.tags.h5("actionButton with CSS class:"), 22 | ui.input_action_button( 23 | "action2", "Action button", class_="btn-primary" 24 | ), 25 | ), 26 | ui.panel_main( 27 | ui.navset_tab( 28 | ui.nav( 29 | "Tab 1", 30 | ui.tags.h4("Table"), 31 | ui.output_table("table"), 32 | ui.tags.h4("Verbatim text output"), 33 | ui.output_code("txtout"), 34 | ui.tags.h1("Header 1"), 35 | ui.tags.h2("Header 2"), 36 | ui.tags.h3("Header 3"), 37 | ui.tags.h4("Header 4"), 38 | ui.tags.h5("Header 5"), 39 | ), 40 | ui.nav("Tab 2"), 41 | ui.nav("Tab 3"), 42 | ) 43 | ), 44 | ), 45 | ), 46 | ui.nav("Plot"), 47 | ui.nav("Table"), 48 | title="Shinyswatch", 49 | ) 50 | 51 | 52 | def server(input, output, session): 53 | @output 54 | @render.code 55 | def txtout(): 56 | return f"{input.txt()}, {input.slider()}, {input.slider()}" 57 | 58 | @output 59 | @render.table 60 | def table(): 61 | import pandas as pd 62 | 63 | cars = pd.DataFrame( 64 | { 65 | "speed": [4, 4, 7, 7, 8, 9], 66 | "dist": [2, 10, 4, 22, 16, 10], 67 | } 68 | ) 69 | return cars.head(4) 70 | 71 | 72 | app = App(app_ui, server) 73 | -------------------------------------------------------------------------------- /examples/python/shinyswatch/requirements.txt: -------------------------------------------------------------------------------- 1 | shinyswatch 2 | -------------------------------------------------------------------------------- /examples/python/static_content/about.txt: -------------------------------------------------------------------------------- 1 | Static content 2 | Serve files from a subdirectory 3 | -------------------------------------------------------------------------------- /examples/python/static_content/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from shiny import App, render, ui 4 | 5 | app_ui = ui.page_fluid( 6 | ui.row( 7 | ui.column( 8 | 6, ui.input_slider("n", "Make a Shiny square:", min=0, max=6, value=2) 9 | ), 10 | ui.column( 11 | 6, 12 | ui.output_ui("images"), 13 | ), 14 | ) 15 | ) 16 | 17 | 18 | def square(x: ui.TagChild, n: int) -> ui.Tag: 19 | row = ui.div([x] * n) 20 | return ui.div([row] * n) 21 | 22 | 23 | def server(input, output, session): 24 | @output 25 | @render.ui 26 | def images() -> ui.Tag: 27 | img = ui.img(src="logo.png", style="width: 40px;") 28 | return square(img, input.n()) 29 | 30 | 31 | www_dir = Path(__file__).parent / "www" 32 | app = App(app_ui, server, static_assets=www_dir) 33 | -------------------------------------------------------------------------------- /examples/python/static_content/www/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/shinylive/8acb41b0d4090308551e2f30dccb68fa6c9071a9/examples/python/static_content/www/logo.png -------------------------------------------------------------------------------- /examples/python/wordle/about.txt: -------------------------------------------------------------------------------- 1 | Wordle 2 | A clone of Wordle 3 | -------------------------------------------------------------------------------- /examples/python/wordle/style.css: -------------------------------------------------------------------------------- 1 | .container-fluid { 2 | text-align: center; 3 | height: calc(100vh - 30px); 4 | display: grid; 5 | grid-template-rows: 1fr auto; 6 | } 7 | .guesses { 8 | overflow-y: auto; 9 | height: 100%; 10 | } 11 | .guesses.finished { 12 | overflow-y: visible; 13 | } 14 | .guesses .word { 15 | margin: 5px; 16 | } 17 | .guesses .word > .letter { 18 | display: inline-block; 19 | width: 50px; 20 | height: 50px; 21 | text-align: center; 22 | vertical-align: middle; 23 | border-radius: 3px; 24 | line-height: 50px; 25 | font-size: 32px; 26 | font-weight: bold; 27 | vertical-align: middle; 28 | user-select: none; 29 | color: white; 30 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 31 | } 32 | .guesses .word > .correct { 33 | background-color: #6a5; 34 | } 35 | .guesses .word > .in-word { 36 | background-color: #db5; 37 | } 38 | .guesses .word > .not-in-word { 39 | background-color: #888; 40 | } 41 | .guesses .word > .guess { 42 | color: black; 43 | background-color: white; 44 | border: 1px solid black; 45 | } 46 | .keyboard { 47 | height: 240px; 48 | user-select: none; 49 | } 50 | .keyboard .keyboard-row { 51 | margin: 3px; 52 | } 53 | .keyboard .keyboard-row .key { 54 | display: inline-block; 55 | padding: 0; 56 | width: 30px; 57 | height: 50px; 58 | text-align: center; 59 | vertical-align: middle; 60 | border-radius: 3px; 61 | line-height: 50px; 62 | font-size: 18px; 63 | font-weight: bold; 64 | vertical-align: middle; 65 | color: black; 66 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 67 | background-color: #ddd; 68 | touch-action: none; 69 | } 70 | .keyboard .keyboard-row .key:focus { 71 | outline: none; 72 | } 73 | .keyboard .keyboard-row .key.wide-key { 74 | font-size: 15px; 75 | width: 50px; 76 | } 77 | .keyboard .keyboard-row .key.correct { 78 | background-color: #6a5; 79 | color: white; 80 | } 81 | .keyboard .keyboard-row .key.in-word { 82 | background-color: #db5; 83 | color: white; 84 | } 85 | .keyboard .keyboard-row .key.not-in-word { 86 | background-color: #888; 87 | color: white; 88 | } 89 | .endgame-content { 90 | font-family: Helvetica, Arial, sans-serif; 91 | display: inline-block; 92 | line-height: 1.4; 93 | letter-spacing: 0.2em; 94 | margin: 20px 8px; 95 | width: fit-content; 96 | padding: 20px; 97 | border-radius: 5px; 98 | box-shadow: 4px 4px 19px rgb(0 0 0 / 17%); 99 | } 100 | -------------------------------------------------------------------------------- /examples/r/001-hello/about.txt: -------------------------------------------------------------------------------- 1 | Hello Shiny! 2 | -------------------------------------------------------------------------------- /examples/r/001-hello/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for app that draws a histogram ---- 5 | ui <- page_sidebar( 6 | 7 | # App title ---- 8 | title = "Hello Shiny!", 9 | 10 | # Sidebar panel for inputs ---- 11 | sidebar = sidebar( 12 | 13 | # Input: Slider for the number of bins ---- 14 | sliderInput( 15 | inputId = "bins", 16 | label = "Number of bins:", 17 | min = 1, 18 | max = 50, 19 | value = 30 20 | ) 21 | ), 22 | 23 | # Output: Histogram ---- 24 | plotOutput(outputId = "distPlot") 25 | ) 26 | 27 | # Define server logic required to draw a histogram ---- 28 | server <- function(input, output) { 29 | 30 | # Histogram of the Old Faithful Geyser Data ---- 31 | # with requested number of bins 32 | # This expression that generates a histogram is wrapped in a call 33 | # to renderPlot to indicate that: 34 | # 35 | # 1. It is "reactive" and therefore should be automatically 36 | # re-executed when inputs (input$bins) change 37 | # 2. Its output type is a plot 38 | output$distPlot <- renderPlot({ 39 | x <- faithful$waiting 40 | bins <- seq(min(x), max(x), length.out = input$bins + 1) 41 | 42 | hist( 43 | x, 44 | breaks = bins, 45 | col = "#75AADB", 46 | border = "white", 47 | xlab = "Waiting time to next eruption (in mins)", 48 | main = "Histogram of waiting times" 49 | ) 50 | }) 51 | } 52 | 53 | # Create Shiny app ---- 54 | shinyApp(ui = ui, server = server) 55 | -------------------------------------------------------------------------------- /examples/r/002-text/about.txt: -------------------------------------------------------------------------------- 1 | Shiny Text 2 | -------------------------------------------------------------------------------- /examples/r/002-text/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for dataset viewer app ---- 5 | ui <- page_sidebar( 6 | 7 | # App title ---- 8 | title = "Shiny Text", 9 | 10 | # Sidebar panel for inputs ---- 11 | sidebar = sidebar( 12 | 13 | # Input: Selector for choosing dataset ---- 14 | selectInput( 15 | inputId = "dataset", 16 | label = "Choose a dataset:", 17 | choices = c("rock", "pressure", "cars") 18 | ), 19 | 20 | # Input: Numeric entry for number of obs to view ---- 21 | numericInput( 22 | inputId = "obs", 23 | label = "Number of observations to view:", 24 | value = 10 25 | ) 26 | ), 27 | 28 | # Output: Verbatim text for data summary ---- 29 | verbatimTextOutput("summary"), 30 | 31 | # Output: HTML table with requested number of observations ---- 32 | tableOutput("view") 33 | ) 34 | 35 | # Define server logic to summarize and view selected dataset ---- 36 | server <- function(input, output) { 37 | 38 | # Return the requested dataset ---- 39 | datasetInput <- reactive({ 40 | switch( 41 | input$dataset, 42 | "rock" = rock, 43 | "pressure" = pressure, 44 | "cars" = cars 45 | ) 46 | }) 47 | 48 | # Generate a summary of the dataset ---- 49 | output$summary <- renderPrint({ 50 | dataset <- datasetInput() 51 | summary(dataset) 52 | }) 53 | 54 | # Show the first "n" observations ---- 55 | output$view <- renderTable({ 56 | head(datasetInput(), n = input$obs) 57 | }) 58 | } 59 | 60 | # Create Shiny app ---- 61 | shinyApp(ui = ui, server = server) 62 | -------------------------------------------------------------------------------- /examples/r/003-reactivity/about.txt: -------------------------------------------------------------------------------- 1 | Reactivity 2 | -------------------------------------------------------------------------------- /examples/r/003-reactivity/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for dataset viewer app ---- 5 | ui <- page_sidebar( 6 | 7 | # App title ---- 8 | title = "Reactivity", 9 | 10 | # Sidebar panel for inputs ---- 11 | sidebar = sidebar( 12 | 13 | # Input: Text for providing a caption ---- 14 | # Note: Changes made to the caption in the textInput control 15 | # are updated in the output area immediately as you type 16 | textInput( 17 | inputId = "caption_text", 18 | label = "Caption:", 19 | value = "Data Summary" 20 | ), 21 | 22 | # Input: Selector for choosing dataset ---- 23 | selectInput( 24 | inputId = "dataset", 25 | label = "Choose a dataset:", 26 | choices = c("rock", "pressure", "cars") 27 | ), 28 | 29 | # Input: Numeric entry for number of obs to view ---- 30 | numericInput( 31 | inputId = "obs", 32 | label = "Number of observations to view:", 33 | value = 10 34 | ) 35 | ), 36 | 37 | # Output: Formatted text for caption ---- 38 | h3(textOutput("caption", container = span)), 39 | 40 | # Output: Verbatim text for data summary ---- 41 | verbatimTextOutput("summary"), 42 | 43 | # Output: HTML table with requested number of observations ---- 44 | tableOutput("view") 45 | ) 46 | 47 | # Define server logic to summarize and view selected dataset ---- 48 | server <- function(input, output) { 49 | 50 | # Return the requested dataset ---- 51 | # By declaring datasetInput as a reactive expression we ensure 52 | # that: 53 | # 54 | # 1. It is only called when the inputs it depends on changes 55 | # 2. The computation and result are shared by all the callers, 56 | # i.e. it only executes a single time 57 | datasetInput <- reactive({ 58 | switch( 59 | input$dataset, 60 | "rock" = rock, 61 | "pressure" = pressure, 62 | "cars" = cars 63 | ) 64 | }) 65 | 66 | # Create caption ---- 67 | # The output$caption is computed based on a reactive expression 68 | # that returns input$caption. When the user changes the 69 | # "caption" field: 70 | # 71 | # 1. This function is automatically called to recompute the output 72 | # 2. New caption is pushed back to the browser for re-display 73 | # 74 | # Note that because the data-oriented reactive expressions 75 | # below don't depend on input$caption_text, those expressions are 76 | # NOT called when input$caption_text changes 77 | output$caption <- renderText({ 78 | input$caption_text 79 | }) 80 | 81 | # Generate a summary of the dataset ---- 82 | # The output$summary depends on the datasetInput reactive 83 | # expression, so will be re-executed whenever datasetInput is 84 | # invalidated, i.e. whenever the input$dataset changes 85 | output$summary <- renderPrint({ 86 | dataset <- datasetInput() 87 | summary(dataset) 88 | }) 89 | 90 | # Show the first "n" observations ---- 91 | # The output$view depends on both the databaseInput reactive 92 | # expression and input$obs, so it will be re-executed whenever 93 | # input$dataset or input$obs is changed 94 | output$view <- renderTable({ 95 | head(datasetInput(), n = input$obs) 96 | }) 97 | } 98 | 99 | # Create Shiny app ---- 100 | shinyApp(ui, server) 101 | -------------------------------------------------------------------------------- /examples/r/004-mpg/about.txt: -------------------------------------------------------------------------------- 1 | Miles Per Gallon 2 | -------------------------------------------------------------------------------- /examples/r/004-mpg/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | library(datasets) 4 | 5 | # Data pre-processing ---- 6 | # Tweak the "am" variable to have nicer factor labels -- since this 7 | # doesn't rely on any user inputs, we can do this once at startup 8 | # and then use the value throughout the lifetime of the app 9 | mpgData <- mtcars 10 | mpgData$am <- factor(mpgData$am, labels = c("Automatic", "Manual")) 11 | 12 | 13 | # Define UI for miles per gallon app ---- 14 | ui <- page_sidebar( 15 | 16 | # App title ---- 17 | title = "Miles Per Gallon", 18 | 19 | # Sidebar panel for inputs ---- 20 | sidebar = sidebar( 21 | 22 | # Input: Selector for variable to plot against mpg ---- 23 | selectInput( 24 | "variable", 25 | "Variable:", 26 | c( 27 | "Cylinders" = "cyl", 28 | "Transmission" = "am", 29 | "Gears" = "gear" 30 | ) 31 | ), 32 | 33 | # Input: Checkbox for whether outliers should be included ---- 34 | checkboxInput("outliers", "Show outliers", TRUE) 35 | ), 36 | 37 | # Output: Formatted text for caption ---- 38 | h3(textOutput("caption")), 39 | 40 | # Output: Plot of the requested variable against mpg ---- 41 | plotOutput("mpgPlot") 42 | ) 43 | 44 | # Define server logic to plot various variables against mpg ---- 45 | server <- function(input, output) { 46 | 47 | # Compute the formula text ---- 48 | # This is in a reactive expression since it is shared by the 49 | # output$caption and output$mpgPlot functions 50 | formulaText <- reactive({ 51 | paste("mpg ~", input$variable) 52 | }) 53 | 54 | # Return the formula text for printing as a caption ---- 55 | output$caption <- renderText({ 56 | formulaText() 57 | }) 58 | 59 | # Generate a plot of the requested variable against mpg ---- 60 | # and only exclude outliers if requested 61 | output$mpgPlot <- renderPlot({ 62 | boxplot( 63 | as.formula(formulaText()), 64 | data = mpgData, 65 | outline = input$outliers, 66 | col = "#75AADB", 67 | pch = 19 68 | ) 69 | }) 70 | } 71 | 72 | # Create Shiny app ---- 73 | shinyApp(ui, server) 74 | -------------------------------------------------------------------------------- /examples/r/005-sliders/about.txt: -------------------------------------------------------------------------------- 1 | Sliders 2 | -------------------------------------------------------------------------------- /examples/r/005-sliders/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for slider demo app ---- 5 | ui <- page_sidebar( 6 | 7 | # App title ---- 8 | title = "Sliders", 9 | 10 | # Sidebar panel for inputs ---- 11 | sidebar = sidebar( 12 | 13 | # Input: Simple integer interval ---- 14 | sliderInput( 15 | "integer", 16 | "Integer:", 17 | min = 0, 18 | max = 1000, 19 | value = 500 20 | ), 21 | 22 | # Input: Decimal interval with step value ---- 23 | sliderInput( 24 | "decimal", 25 | "Decimal:", 26 | min = 0, 27 | max = 1, 28 | value = 0.5, 29 | step = 0.1 30 | ), 31 | 32 | # Input: Specification of range within an interval ---- 33 | sliderInput( 34 | "range", 35 | "Range:", 36 | min = 1, 37 | max = 1000, 38 | value = c(200, 500) 39 | ), 40 | 41 | # Input: Custom currency format for with basic animation ---- 42 | sliderInput( 43 | "format", 44 | "Custom Format:", 45 | min = 0, 46 | max = 10000, 47 | value = 0, 48 | step = 2500, 49 | pre = "$", 50 | sep = ",", 51 | animate = TRUE 52 | ), 53 | 54 | # Input: Animation with custom interval (in ms) ---- 55 | # to control speed, plus looping 56 | sliderInput( 57 | "animation", 58 | "Looping Animation:", 59 | min = 1, 60 | max = 2000, 61 | value = 1, 62 | step = 10, 63 | animate = 64 | animationOptions(interval = 300, loop = TRUE) 65 | ) 66 | ), 67 | 68 | # Output: Table summarizing the values entered ---- 69 | tableOutput("values") 70 | ) 71 | 72 | # Define server logic for slider examples ---- 73 | server <- function(input, output) { 74 | 75 | # Reactive expression to create data frame of all input values ---- 76 | sliderValues <- reactive({ 77 | data.frame( 78 | Name = c( 79 | "Integer", 80 | "Decimal", 81 | "Range", 82 | "Custom Format", 83 | "Animation" 84 | ), 85 | Value = as.character(c( 86 | input$integer, 87 | input$decimal, 88 | paste(input$range, collapse = " "), 89 | input$format, 90 | input$animation 91 | )), 92 | stringsAsFactors = FALSE 93 | ) 94 | }) 95 | 96 | # Show the values in an HTML table ---- 97 | output$values <- renderTable({ 98 | sliderValues() 99 | }) 100 | } 101 | 102 | # Create Shiny app ---- 103 | shinyApp(ui, server) 104 | -------------------------------------------------------------------------------- /examples/r/006-tabsets/about.txt: -------------------------------------------------------------------------------- 1 | Tabsets 2 | -------------------------------------------------------------------------------- /examples/r/006-tabsets/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for random distribution app ---- 5 | # Sidebar layout with input and output definitions ---- 6 | ui <- page_sidebar( 7 | 8 | # App title ---- 9 | title = "Tabsets", 10 | 11 | # Sidebar panel for inputs ---- 12 | sidebar = sidebar( 13 | 14 | # Input: Select the random distribution type ---- 15 | radioButtons( 16 | "dist", 17 | "Distribution type:", 18 | c( 19 | "Normal" = "norm", 20 | "Uniform" = "unif", 21 | "Log-normal" = "lnorm", 22 | "Exponential" = "exp" 23 | ) 24 | ), 25 | # br() element to introduce extra vertical spacing ---- 26 | br(), 27 | # Input: Slider for the number of observations to generate ---- 28 | sliderInput( 29 | "n", 30 | "Number of observations:", 31 | value = 500, 32 | min = 1, 33 | max = 1000 34 | ) 35 | ), 36 | 37 | # Main panel for displaying outputs ---- 38 | # Output: A tabset that combines three panels ---- 39 | navset_card_underline( 40 | # Panel with plot ---- 41 | nav_panel("Plot", plotOutput("plot")), 42 | 43 | # Panel with summary ---- 44 | nav_panel("Summary", verbatimTextOutput("summary")), 45 | 46 | # Panel with table ---- 47 | nav_panel("Table", tableOutput("table")) 48 | ) 49 | ) 50 | 51 | # Define server logic for random distribution app ---- 52 | server <- function(input, output) { 53 | 54 | # Reactive expression to generate the requested distribution ---- 55 | # This is called whenever the inputs change. The output functions 56 | # defined below then use the value computed from this expression 57 | d <- reactive({ 58 | dist <- switch( 59 | input$dist, 60 | norm = rnorm, 61 | unif = runif, 62 | lnorm = rlnorm, 63 | exp = rexp, 64 | rnorm 65 | ) 66 | 67 | dist(input$n) 68 | }) 69 | 70 | # Generate a plot of the data ---- 71 | # Also uses the inputs to build the plot label. Note that the 72 | # dependencies on the inputs and the data reactive expression are 73 | # both tracked, and all expressions are called in the sequence 74 | # implied by the dependency graph. 75 | output$plot <- renderPlot({ 76 | dist <- input$dist 77 | n <- input$n 78 | 79 | hist( 80 | d(), 81 | main = paste("r", dist, "(", n, ")", sep = ""), 82 | col = "#75AADB", 83 | border = "white" 84 | ) 85 | }) 86 | 87 | # Generate a summary of the data ---- 88 | output$summary <- renderPrint({ 89 | summary(d()) 90 | }) 91 | 92 | # Generate an HTML table view of the data ---- 93 | output$table <- renderTable({ 94 | d() 95 | }) 96 | } 97 | 98 | # Create Shiny app ---- 99 | shinyApp(ui, server) 100 | -------------------------------------------------------------------------------- /examples/r/007-widgets/about.txt: -------------------------------------------------------------------------------- 1 | Widgets 2 | -------------------------------------------------------------------------------- /examples/r/007-widgets/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for slider demo app ---- 5 | ui <- page_sidebar( 6 | 7 | # App title ---- 8 | title = "More Widgets", 9 | 10 | # Sidebar panel for inputs ---- 11 | sidebar = sidebar( 12 | 13 | # Input: Select a dataset ---- 14 | selectInput( 15 | "dataset", 16 | "Choose a dataset:", 17 | choices = c("rock", "pressure", "cars") 18 | ), 19 | 20 | # Input: Specify the number of observations to view ---- 21 | numericInput("obs", "Number of observations to view:", 10), 22 | 23 | # Include clarifying text ---- 24 | helpText( 25 | "Note: while the data view will show only the specified", 26 | "number of observations, the summary will still be based", 27 | "on the full dataset." 28 | ), 29 | 30 | # Input: actionButton() to defer the rendering of output ---- 31 | # until the user explicitly clicks the button (rather than 32 | # doing it immediately when inputs change). This is useful if 33 | # the computations required to render output are inordinately 34 | # time-consuming. 35 | actionButton("update", "Update View") 36 | ), 37 | 38 | # Output: Header + summary of distribution ---- 39 | h4("Summary"), 40 | verbatimTextOutput("summary"), 41 | 42 | # Output: Header + table of distribution ---- 43 | h4("Observations"), 44 | tableOutput("view") 45 | ) 46 | 47 | # Define server logic to summarize and view selected dataset ---- 48 | server <- function(input, output) { 49 | 50 | # Return the requested dataset ---- 51 | # Note that we use eventReactive() here, which depends on 52 | # input$update (the action button), so that the output is only 53 | # updated when the user clicks the button 54 | datasetInput <- eventReactive( 55 | input$update, 56 | { 57 | switch( 58 | input$dataset, 59 | "rock" = rock, 60 | "pressure" = pressure, 61 | "cars" = cars 62 | ) 63 | }, 64 | ignoreNULL = FALSE 65 | ) 66 | 67 | # Generate a summary of the dataset ---- 68 | output$summary <- renderPrint({ 69 | dataset <- datasetInput() 70 | summary(dataset) 71 | }) 72 | 73 | # Show the first "n" observations ---- 74 | # The use of isolate() is necessary because we don't want the table 75 | # to update whenever input$obs changes (only when the user clicks 76 | # the action button) 77 | output$view <- renderTable({ 78 | head(datasetInput(), n = isolate(input$obs)) 79 | }) 80 | } 81 | 82 | # Create Shiny app ---- 83 | shinyApp(ui, server) 84 | -------------------------------------------------------------------------------- /examples/r/008-html/about.txt: -------------------------------------------------------------------------------- 1 | Custom HTML UI 2 | -------------------------------------------------------------------------------- /examples/r/008-html/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | # Define server logic for random distribution app ---- 4 | server <- function(input, output) { 5 | 6 | # Reactive expression to generate the requested distribution ---- 7 | # This is called whenever the inputs change. The output functions 8 | # defined below then use the value computed from this expression 9 | d <- reactive({ 10 | dist <- switch( 11 | input$dist, 12 | norm = rnorm, 13 | unif = runif, 14 | lnorm = rlnorm, 15 | exp = rexp, 16 | rnorm 17 | ) 18 | 19 | dist(input$n) 20 | }) 21 | 22 | # Generate a plot of the data ---- 23 | # Also uses the inputs to build the plot label. Note that the 24 | # dependencies on the inputs and the data reactive expression are 25 | # both tracked, and all expressions are called in the sequence 26 | # implied by the dependency graph. 27 | output$plot <- renderPlot({ 28 | dist <- input$dist 29 | n <- input$n 30 | 31 | hist( 32 | d(), 33 | main = paste("r", dist, "(", n, ")", sep = ""), 34 | col = "#75AADB", 35 | border = "white" 36 | ) 37 | }) 38 | 39 | # Generate a summary of the data ---- 40 | output$summary <- renderPrint({ 41 | summary(d()) 42 | }) 43 | 44 | # Generate an HTML table view of the head of the data ---- 45 | output$table <- renderTable({ 46 | head(data.frame(x = d())) 47 | }) 48 | } 49 | 50 | # Create Shiny app ---- 51 | shinyApp(ui = htmlTemplate("www/index.html"), server) 52 | -------------------------------------------------------------------------------- /examples/r/008-html/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |

HTML UI

12 | 13 |

14 |
15 | 21 |

22 | 23 |

24 | 25 |
26 | 27 | 28 |

29 | 30 |

Summary of data:

31 |

32 | 
33 |   

Plot of data:

34 |
36 | 37 |

Head of data:

38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/r/009-upload/about.txt: -------------------------------------------------------------------------------- 1 | R File Upload 2 | -------------------------------------------------------------------------------- /examples/r/009-upload/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for slider demo app ---- 5 | ui <- page_sidebar( 6 | 7 | # App title ---- 8 | title = "Uploading Files", 9 | 10 | # Sidebar panel for inputs ---- 11 | sidebar = sidebar( 12 | 13 | # Input: Select a file ---- 14 | fileInput( 15 | "file1", 16 | "Choose CSV File", 17 | multiple = TRUE, 18 | accept = c( 19 | "text/csv", 20 | "text/comma-separated-values,text/plain", 21 | ".csv" 22 | ) 23 | ), 24 | 25 | # Horizontal line ---- 26 | tags$hr(), 27 | 28 | # Input: Checkbox if file has header ---- 29 | checkboxInput("header", "Header", TRUE), 30 | 31 | # Input: Select separator ---- 32 | radioButtons( 33 | "sep", 34 | "Separator", 35 | choices = c( 36 | Comma = ",", 37 | Semicolon = ";", 38 | Tab = "\t" 39 | ), 40 | selected = "," 41 | ), 42 | 43 | # Input: Select quotes ---- 44 | radioButtons( 45 | "quote", 46 | "Quote", 47 | choices = c( 48 | None = "", 49 | "Double Quote" = '"', 50 | "Single Quote" = "'" 51 | ), 52 | selected = '"' 53 | ), 54 | 55 | # Horizontal line ---- 56 | tags$hr(), 57 | 58 | # Input: Select number of rows to display ---- 59 | radioButtons( 60 | "disp", 61 | "Display", 62 | choices = c( 63 | Head = "head", 64 | All = "all" 65 | ), 66 | selected = "head" 67 | ) 68 | ), 69 | 70 | # Output: Data file ---- 71 | tableOutput("contents") 72 | ) 73 | 74 | # Define server logic to read selected file ---- 75 | server <- function(input, output) { 76 | output$contents <- renderTable({ 77 | # input$file1 will be NULL initially. After the user selects 78 | # and uploads a file, head of that data file by default, 79 | # or all rows if selected, will be shown. 80 | 81 | req(input$file1) 82 | 83 | df <- read.csv( 84 | input$file1$datapath, 85 | header = input$header, 86 | sep = input$sep, 87 | quote = input$quote 88 | ) 89 | 90 | if (input$disp == "head") { 91 | return(head(df)) 92 | } else { 93 | return(df) 94 | } 95 | }) 96 | } 97 | 98 | # Create Shiny app ---- 99 | shinyApp(ui, server) 100 | -------------------------------------------------------------------------------- /examples/r/010-download/about.txt: -------------------------------------------------------------------------------- 1 | R File Download 2 | -------------------------------------------------------------------------------- /examples/r/010-download/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Workaround for Chromium Issue 468227 5 | downloadButton <- function(...) { 6 | tag <- shiny::downloadButton(...) 7 | tag$attribs$download <- NULL 8 | tag 9 | } 10 | 11 | # Define UI for slider demo app ---- 12 | ui <- page_sidebar( 13 | 14 | # App title ---- 15 | title = "Downloading Data", 16 | 17 | # Sidebar panel for inputs ---- 18 | sidebar = sidebar( 19 | 20 | # Input: Choose dataset ---- 21 | selectInput( 22 | "dataset", 23 | "Choose a dataset:", 24 | choices = c("rock", "pressure", "cars") 25 | ), 26 | 27 | # Button 28 | downloadButton("downloadData", "Download") 29 | ), 30 | tableOutput("table") 31 | ) 32 | 33 | # Define server logic to display and download selected file ---- 34 | server <- function(input, output) { 35 | 36 | # Reactive value for selected dataset ---- 37 | datasetInput <- reactive({ 38 | switch( 39 | input$dataset, 40 | "rock" = rock, 41 | "pressure" = pressure, 42 | "cars" = cars 43 | ) 44 | }) 45 | 46 | # Table of selected dataset ---- 47 | output$table <- renderTable({ 48 | datasetInput() 49 | }) 50 | 51 | # Downloadable csv of selected dataset ---- 52 | output$downloadData <- downloadHandler( 53 | filename = function() { 54 | paste(input$dataset, ".csv", sep = "") 55 | }, 56 | content = function(file) { 57 | write.csv(datasetInput(), file, row.names = FALSE) 58 | } 59 | ) 60 | } 61 | 62 | # Create Shiny app ---- 63 | shinyApp(ui, server) 64 | -------------------------------------------------------------------------------- /examples/r/011-timer/about.txt: -------------------------------------------------------------------------------- 1 | Timer 2 | -------------------------------------------------------------------------------- /examples/r/011-timer/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | 4 | # Define UI for displaying current time ---- 5 | ui <- page_fluid( 6 | h2(textOutput("currentTime")) 7 | ) 8 | 9 | # Define server logic to show current time, update every second ---- 10 | server <- function(input, output, session) { 11 | output$currentTime <- renderText({ 12 | invalidateLater(1000, session) 13 | paste("The current time is", Sys.time()) 14 | }) 15 | } 16 | 17 | # Create Shiny app ---- 18 | shinyApp(ui, server) 19 | -------------------------------------------------------------------------------- /export_template/edit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirect to editable app 5 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /export_template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{#title}} 7 | {{.}} 8 | {{/title}} 9 | 13 | 21 | 22 | 23 | {{{ include_in_head }}} 24 | 25 | 26 | {{{ include_before_body }}} 27 |
28 | {{{ include_after_body }}} 29 | 30 | 31 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1730602179, 6 | "narHash": "sha256-efgLzQAWSzJuCLiCaQUCDu4NudNlHdg2NzGLX5GYaEY=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "3c2f1c4ca372622cb2f9de8016c9a0b1cbd0f37c", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-24.05", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Shinylive web assets"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; 6 | }; 7 | 8 | outputs = 9 | { self, nixpkgs }: 10 | let 11 | supportedSystems = [ 12 | "x86_64-linux" 13 | "aarch64-darwin" 14 | "x86_64-darwin" 15 | "aarch64-linux" 16 | ]; 17 | forEachSupportedSystem = 18 | f: 19 | nixpkgs.lib.genAttrs supportedSystems ( 20 | system: 21 | f rec { 22 | pkgs = import nixpkgs { inherit system; }; 23 | inherit system; 24 | } 25 | ); 26 | 27 | in 28 | { 29 | packages = forEachSupportedSystem ( 30 | { pkgs, system, ... }: 31 | { 32 | default = pkgs.stdenv.mkDerivation { 33 | name = "shinylive"; 34 | src = ./.; 35 | 36 | # TODO: 37 | # - cache yarn packages 38 | # - cache pyodide tarball 39 | 40 | nativeBuildInputs = with pkgs; [ 41 | nodejs_20 42 | 43 | python312 44 | (with python312Packages; [ 45 | pip 46 | virtualenv 47 | ]) 48 | 49 | curl 50 | cacert 51 | git 52 | ]; 53 | 54 | buildPhase = '' 55 | export HOME=$PWD 56 | make all 57 | ''; 58 | 59 | installPhase = '' 60 | mkdir -p $out 61 | cp -r build/* $out 62 | ''; 63 | }; 64 | } 65 | ); 66 | 67 | devShells = forEachSupportedSystem ( 68 | { pkgs, system }: 69 | { 70 | default = pkgs.mkShell { 71 | 72 | # Get the nativeBuildInputs from packages.default 73 | inputsFrom = [ self.packages.${system}.default ]; 74 | 75 | packages = with pkgs; [ 76 | nodePackages.npm-check-updates 77 | 78 | ]; 79 | }; 80 | } 81 | ); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | import { devices } from "@playwright/test"; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config(); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | const config: PlaywrightTestConfig = { 14 | testDir: "./playwright", 15 | /* Maximum time one test can run for. */ 16 | timeout: 50 * 1000, 17 | expect: { 18 | /** 19 | * Maximum time expect() should wait for the condition to be met. 20 | * For example in `await expect(locator).toHaveText();` 21 | */ 22 | timeout: 20000, 23 | }, 24 | /* Run tests in files in parallel */ 25 | fullyParallel: true, 26 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 27 | forbidOnly: !!process.env.CI, 28 | /* Retry on CI only */ 29 | retries: process.env.CI ? 2 : 1, 30 | /* Opt out of parallel tests on CI. */ 31 | workers: process.env.CI ? 1 : undefined, 32 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 33 | reporter: "html", 34 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 35 | use: { 36 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 37 | actionTimeout: 0, 38 | /* Base URL to use in actions like `await page.goto('/')`. */ 39 | baseURL: "http://localhost:3000", 40 | 41 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 42 | trace: "on-first-retry", 43 | }, 44 | 45 | /* Configure projects for major browsers */ 46 | projects: [ 47 | { 48 | name: "chromium", 49 | use: { 50 | ...devices["Desktop Chrome"], 51 | }, 52 | }, 53 | 54 | // { 55 | // name: "firefox", 56 | // use: { 57 | // ...devices["Desktop Firefox"], 58 | // }, 59 | // }, 60 | 61 | // { 62 | // name: "webkit", 63 | // use: { 64 | // ...devices["Desktop Safari"], 65 | // }, 66 | // }, 67 | ], 68 | 69 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 70 | // outputDir: 'test-results/', 71 | 72 | /* Run your local dev server before starting the tests */ 73 | webServer: [ 74 | { 75 | command: "npm run test-server", 76 | port: 3000, 77 | }, 78 | { 79 | command: ` 80 | . venv/bin/activate && 81 | mkdir -p playwright/static-build && 82 | shinylive export playwright/static-app-test playwright/static-build/app && 83 | python3 -u -m http.server --directory playwright/static-build/app 8008 2> /dev/null 84 | `, 85 | port: 8008, 86 | }, 87 | { 88 | command: ` 89 | . venv/bin/activate && 90 | mkdir -p playwright/static-build && 91 | shinylive export playwright/editor-cell-test playwright/static-build/cell && 92 | sed -i.bak -e 's/app\.py/code.py/' playwright/static-build/cell/app.json && 93 | sed -i.bak -e 's/viewer/editor-cell/' playwright/static-build/cell/index.html && 94 | python3 -u -m http.server --directory playwright/static-build/cell 8009 2> /dev/null 95 | `, 96 | port: 8009, 97 | }, 98 | ], 99 | }; 100 | 101 | export default config; 102 | -------------------------------------------------------------------------------- /playwright/editor-cell-test/app.py: -------------------------------------------------------------------------------- 1 | 123 + 456 2 | -------------------------------------------------------------------------------- /playwright/editor-cell.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect_editor_has_text, expect_output_has_text } from "./helpers"; 2 | import { expect, test } from "@playwright/test"; 3 | 4 | test.use({ baseURL: "http://localhost:8009" }); 5 | 6 | test.describe("Editor cell view in static deployment", async () => { 7 | test("Input text editor is populated", async ({ page }) => { 8 | await page.goto(`/`); 9 | 10 | await expect_editor_has_text(page, "123 + 456") 11 | }); 12 | 13 | test("Result is computed and shown in output frame", async ({ page }) => { 14 | await page.goto(`/`); 15 | 16 | await expect(page.locator("pre.output-content")).toBeVisible(); 17 | await expect_output_has_text(page, "579") 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /playwright/examples-viewer.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect_terminal_has_text, wait_until_initialized } from "./helpers"; 2 | import { expect, test } from "@playwright/test"; 3 | 4 | test("Open examples page and click to a new example", async ({ page }) => { 5 | await page.goto("/examples/"); 6 | // Click text=App with plot 7 | await page.locator("text=App with plot").click(); 8 | await expect(page).toHaveURL("http://localhost:3000/examples/#app-with-plot"); 9 | }); 10 | 11 | test("Add a new non-app script, type in it, and run code", async ({ page }) => { 12 | await page.goto("/examples/"); 13 | 14 | // Wait for initialization to complete 15 | await wait_until_initialized(page); 16 | await page.locator(`[aria-label="Add a file"]`).click(); 17 | await page.locator('[aria-label="Name current file"]').fill("my_app.py"); 18 | 19 | await page.locator(".cm-editor [role=textbox]").type(`print("hello world")`); 20 | 21 | // Running both command enter for mac and control enter for non-macs. Running 22 | // both just helps avoid looking at running environment 23 | await page.locator(".cm-editor [role=textbox]").press(`Meta+Enter`); 24 | await page.locator(".cm-editor [role=textbox]").press(`Control+Enter`); 25 | 26 | // Make sure that hello world exists in the terminal output 27 | await expect_terminal_has_text(page, `>>> print("hello world")`); 28 | await expect_terminal_has_text(page, "hello world"); 29 | }); 30 | -------------------------------------------------------------------------------- /playwright/load-from-url.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app_url_encoding, 3 | expect_app_has_text, 4 | expect_editor_has_text, 5 | } from "./helpers"; 6 | import { expect, test } from "@playwright/test"; 7 | 8 | test.describe("The URL can be used to load data", async () => { 9 | test("Editor view", async ({ page }) => { 10 | await page.goto(`/editor/#${app_url_encoding}`); 11 | 12 | // Make sure the correct text is in the editor 13 | await expect_editor_has_text(page, 'ui.h1("Code from a url")'); 14 | 15 | await expect_app_has_text(page, "Code from a url", "h1"); 16 | }); 17 | 18 | test("Examples view", async ({ page }) => { 19 | await page.goto(`/examples/#${app_url_encoding}`); 20 | 21 | // Sanity check we're in the examples view. 22 | await expect(page.locator("text=/^examples$/i")).toBeVisible(); 23 | 24 | // Make sure the correct text is in the editor 25 | await expect_editor_has_text(page, 'ui.h1("Code from a url")'); 26 | 27 | await expect_app_has_text(page, "Code from a url", "h1"); 28 | }); 29 | 30 | test("App view", async ({ page }) => { 31 | await page.goto(`/app/#${app_url_encoding}`); 32 | 33 | await expect_app_has_text(page, "Code from a url", "h1"); 34 | }); 35 | }); 36 | 37 | // Looks for the shiny logo 38 | const header_bar_selector = '.HeaderBar img[alt="Shiny"]'; 39 | 40 | test.describe("The header bar parameter can turn off the header in app view but not the other views", async () => { 41 | test("Editor view can't turn off header bar", async ({ page }) => { 42 | await page.goto(`/editor/#h=0&${app_url_encoding}`); 43 | 44 | await expect(page.locator(header_bar_selector)).toBeVisible(); 45 | }); 46 | test("Examples view can't turn off header bar", async ({ page }) => { 47 | await page.goto(`/examples/#h=0&${app_url_encoding}`); 48 | 49 | await expect(page.locator(header_bar_selector)).toBeVisible(); 50 | }); 51 | 52 | test("App view can turn off header bar", async ({ page }) => { 53 | await page.goto(`/app/#h=0&${app_url_encoding}`); 54 | 55 | await expect(page.locator(header_bar_selector)).not.toBeVisible(); 56 | }); 57 | test("App view can show header bar", async ({ page }) => { 58 | await page.goto(`/editor/#${app_url_encoding}`); 59 | 60 | await expect(page.locator(header_bar_selector)).toBeVisible(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /playwright/shiny-static.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect_editor_has_text, app_url_encoding } from "./helpers"; 2 | import { expect, test } from "@playwright/test"; 3 | 4 | test.use({ baseURL: "http://localhost:8008" }); 5 | 6 | test.describe("Shiny-Static deploys", async () => { 7 | test("Main app view is base of URL", async ({ page }) => { 8 | await page.goto(`/`); 9 | 10 | await expect( 11 | page.frameLocator(".app-frame").locator("text=Hello Shiny-Static!") 12 | ).toBeVisible(); 13 | }); 14 | 15 | test("Edit view is accessable via the edit path", async ({ page }) => { 16 | await page.goto(`/edit`); 17 | 18 | // Make sure editor is there and has the identifying header 19 | await expect_editor_has_text(page, `ui.h2("Hello Shiny-Static!")`); 20 | 21 | await expect( 22 | page.frameLocator(".app-frame").locator("text=Hello Shiny-Static!") 23 | ).toBeVisible(); 24 | }); 25 | 26 | test("Doesn't load data from URL", async ({ page }) => { 27 | await page.goto(`/edit/#${app_url_encoding}`); 28 | 29 | // Make sure editor is there and has the identifying header 30 | await expect( 31 | page.locator(`.shinylive-editor`, { 32 | hasText: `ui.h2("Hello Shiny-Static!")`, 33 | }) 34 | ).toBeVisible(); 35 | 36 | // Double check that the header from the app from url encoding didnt make it 37 | // into the editor 38 | await expect( 39 | page.locator(`.shinylive-editor`, { hasText: 'ui.h1("Code from a url")' }) 40 | ).not.toBeVisible(); 41 | 42 | await expect( 43 | page.frameLocator(".app-frame").locator("text=Hello Shiny-Static!") 44 | ).toBeVisible(); 45 | }); 46 | 47 | test("App view never shows header bar - no url req", async ({ page }) => { 48 | await page.goto(`/`); 49 | 50 | await expect(page.locator('.HeaderBar img[alt="Shiny"]')).not.toBeVisible(); 51 | }); 52 | 53 | test("App view never shows header bar - with url req", async ({ page }) => { 54 | await page.goto(`/#h=0`); 55 | 56 | await expect(page.locator('.HeaderBar img[alt="Shiny"]')).not.toBeVisible(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /playwright/static-app-test/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, render, ui 2 | 3 | app_ui = ui.page_fluid( 4 | ui.h2("Hello Shiny-Static!"), 5 | ui.input_slider("n", "N", 0, 100, 20), 6 | ui.output_text_verbatim("txt"), 7 | ) 8 | 9 | 10 | def server(input, output, session): 11 | @render.text 12 | def txt(): 13 | return f"n*2 is {input.n() * 2}" 14 | 15 | 16 | app = App(app_ui, server) 17 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "typeCheckingMode": "basic", 3 | "strict": ["scripts"], 4 | "reportUnusedFunction": "none", 5 | "reportWildcardImportFromLibrary": "none" 6 | } 7 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pkginfo>=1.8.2 2 | packaging>=21.3 3 | requirements-parser>=0.5.0 4 | typing-extensions 5 | pyright 6 | shinyswatch 7 | -------------------------------------------------------------------------------- /shinylive_requirements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "name": "htmltools", "source": "local", "version": "latest" }, 3 | { "name": "shiny", "source": "local", "version": "latest" }, 4 | { "name": "shinywidgets", "source": "local", "version": "latest" }, 5 | { "name": "shinyswatch", "source": "pypi", "version": "latest" }, 6 | { "name": "faicons", "source": "local", "version": "latest" }, 7 | { 8 | "name": "mdit-py-plugins", 9 | "source": "pypi", 10 | "version": "latest", 11 | "comment": "Needed to suppress warning from shiny/ui/_markdown.py." 12 | }, 13 | { "name": "plotnine", "source": "pypi", "version": "latest" }, 14 | { "name": "plotly", "source": "pypi", "version": "latest" }, 15 | { "name": "seaborn", "source": "pypi", "version": "latest" }, 16 | { "name": "ipywidgets", "source": "pypi", "version": "latest" }, 17 | { "name": "ipyleaflet", "source": "pypi", "version": "latest" }, 18 | { "name": "siuba", "source": "pypi", "version": "latest" }, 19 | { "name": "palmerpenguins", "source": "pypi", "version": "latest" }, 20 | { "name": "black", "source": "pypi", "version": "latest" }, 21 | { "name": "qrcode", "source": "pypi", "version": "latest" }, 22 | { 23 | "name": "libsass", 24 | "source": "local", 25 | "version": "latest", 26 | "comment": "Built for pyodide in https://github.com/gadenbuie/libsass-python/tree/dev" 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /site/app/.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | -------------------------------------------------------------------------------- /site/editor/.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | -------------------------------------------------------------------------------- /site/examples/.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | -------------------------------------------------------------------------------- /site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/shinylive/8acb41b0d4090308551e2f30dccb68fa6c9071a9/site/favicon.ico -------------------------------------------------------------------------------- /site/shinylive: -------------------------------------------------------------------------------- 1 | ../build/shinylive/ -------------------------------------------------------------------------------- /site/shinylive-sw.js: -------------------------------------------------------------------------------- 1 | ../build/shinylive-sw.js -------------------------------------------------------------------------------- /site_template/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shiny App 7 | {{GOOGLE_TAG_MANAGER_SCRIPT}} 8 | 9 | 34 | 35 | 36 | 37 | 38 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /site_template/editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shiny editor 7 | {{GOOGLE_TAG_MANAGER_SCRIPT}} 8 | 9 | 38 | 39 | 40 | 41 | 42 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /site_template/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shiny examples 7 | {{GOOGLE_TAG_MANAGER_SCRIPT}} 8 | 9 | 37 | 38 | 39 | 40 | 41 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Components/ExampleSelector.css: -------------------------------------------------------------------------------- 1 | .shinylive-example-selector { 2 | padding: 3px 3px 3px 8px; 3 | font-family: var(--font-face); 4 | /* Each level of hierarchy will indent by a set amount (via padding) */ 5 | --indent: 8px; 6 | --divider-color: #f1f1f1; 7 | } 8 | 9 | .shinylive-example-selector > .categories { 10 | height: 100%; 11 | overflow-y: auto; 12 | padding: var(--indent); 13 | } 14 | 15 | .shinylive-example-selector > .categories .category-title { 16 | color: var(--colors-blue, blue); 17 | margin-top: calc(var(--indent) * 2); 18 | } 19 | 20 | .shinylive-example-selector .example { 21 | --v-pad: calc(var(--indent)); 22 | --h-pad: calc(var(--indent) * 2); 23 | /* margin: 0.5rem 0; */ 24 | padding-left: var(--h-pad); 25 | padding-top: var(--v-pad); 26 | padding-bottom: var(--v-pad); 27 | cursor: default; 28 | position: relative; 29 | --sidebar-color: var(--colors-grey); 30 | --sidebar-indent: calc(var(--indent) / 1.3); 31 | --sidebar-w: 1px; 32 | } 33 | 34 | /* Undo the default link styles */ 35 | .shinylive-example-selector .example > a { 36 | text-decoration: unset; 37 | color: unset; 38 | cursor: unset; 39 | } 40 | 41 | .shinylive-example-selector .example:hover:not(.selected) { 42 | cursor: pointer; 43 | } 44 | 45 | /* Selection is indicated by a thicker sidebar. Bar grows when hovered to indicate that clicking will select */ 46 | .shinylive-example-selector .example.selected, 47 | .shinylive-example-selector .example:hover { 48 | --sidebar-w: 4px; 49 | } 50 | 51 | /* The selected example's sidebar is blue */ 52 | .shinylive-example-selector .example.selected { 53 | --sidebar-color: var(--colors-blue, blue); 54 | } 55 | 56 | /* The sidebar is defined by a before psuedoelement */ 57 | .shinylive-example-selector .example:before { 58 | content: ""; 59 | position: absolute; 60 | height: 70%; 61 | top: 15%; 62 | width: var(--sidebar-w); 63 | border-radius: var(--button-roundness, 5px); 64 | left: calc(var(--sidebar-indent) - var(--sidebar-w) - 1px); 65 | background-color: var(--sidebar-color); 66 | } 67 | 68 | .shinylive-example-selector .example .title { 69 | font-weight: 500; 70 | } 71 | 72 | .shinylive-example-selector .example .about { 73 | color: var(--colors-grey, grey); 74 | font-size: small; 75 | font-style: italic; 76 | font-weight: 400; 77 | } 78 | 79 | .shinylive-example-selector > .categories > section > .divider { 80 | width: 100%; 81 | height: 1px; 82 | background-color: var(--divider-color); 83 | } 84 | -------------------------------------------------------------------------------- /src/Components/ExampleSelector.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import userEvent from "@testing-library/user-event"; 3 | import React from "react"; 4 | import exampleItems from "../examples.json"; 5 | import ExampleSelector from "./ExampleSelector"; 6 | 7 | const desiredExample = exampleItems[1].apps[1]; 8 | test("Selecting an example will trigger the proper change", () => { 9 | const onFileChange = jest.fn(); 10 | render( 11 | 12 | ); 13 | 14 | // Click on a desired example title 15 | userEvent.click(screen.getByText(desiredExample.title)); 16 | 17 | // Should get the files content of that example back 18 | expect(onFileChange).toHaveBeenLastCalledWith(desiredExample.files); 19 | }); 20 | 21 | test("Trying to switch examples after edits have been made will trigger a confirmation dialog", () => { 22 | const onFileChange = jest.fn(); 23 | window.confirm = jest.fn(); 24 | render( 25 | 26 | ); 27 | 28 | // Click on a desired example title 29 | userEvent.click(screen.getByText(desiredExample.title)); 30 | 31 | // A confirm dialog should popup to let the user know they will throw away edits 32 | expect(window.confirm).toHaveBeenLastCalledWith( 33 | "Discard all changes to files?" 34 | ); 35 | }); 36 | 37 | test("Repeat selections of the same example won't trigger updates", () => { 38 | const onFileChange = jest.fn(); 39 | render( 40 | 41 | ); 42 | 43 | // Click on a desired example title 44 | userEvent.click(screen.getByText(desiredExample.title)); 45 | 46 | // Record how many times the file change mock function has been called by component 47 | const numOnFileChangeCalls = onFileChange.mock.calls.length; 48 | 49 | // Select the same example again 50 | userEvent.click(screen.getByText(desiredExample.title)); 51 | 52 | // Number of times the fileChange callback has been called has not gone up 53 | expect(onFileChange).toHaveBeenCalledTimes(numOnFileChangeCalls); 54 | }); 55 | -------------------------------------------------------------------------------- /src/Components/HeaderBar.css: -------------------------------------------------------------------------------- 1 | .HeaderBar { 2 | height: 30px; 3 | padding: 3px 8px; 4 | background-color: #007BC2; 5 | font-family: var(--font-face); 6 | font-size: 1.1rem; 7 | color: var(--colors-white); 8 | display: flex; 9 | } 10 | 11 | .HeaderBar > a.page-title { 12 | display: inherit; 13 | color: var(--colors-white); 14 | text-decoration: none; 15 | flex-direction: row; 16 | margin-right: auto; 17 | } 18 | 19 | .HeaderBar .shiny-logo { 20 | height: 22px; 21 | display: inline-block; 22 | margin-right: 6px; 23 | margin-left: 4px; 24 | margin-top: 1px; 25 | } 26 | 27 | .HeaderBar > div { 28 | flex-direction: row; 29 | margin-right: 1rem; 30 | } 31 | 32 | .HeaderBar button.code-run-button { 33 | border: none; 34 | font-size: 0.9rem; 35 | padding: 0.2rem 0.5rem; 36 | background-color: transparent; 37 | white-space: nowrap; 38 | fill: rgba(255, 255, 255, 0.8); 39 | } 40 | 41 | .HeaderBar button.code-run-button svg.shinylive-icon { 42 | font-size: 1em; 43 | } 44 | 45 | .HeaderBar button.code-run-button span.button-label { 46 | vertical-align: 0.15em; 47 | margin-left: 0.3em; 48 | } 49 | 50 | .HeaderBar .code-run-button:hover { 51 | color: var(--colors-white); 52 | fill: var(--colors-white); 53 | } 54 | -------------------------------------------------------------------------------- /src/Components/Icons.css: -------------------------------------------------------------------------------- 1 | .shinylive-icon { 2 | display: inline-block; 3 | height: 1em; 4 | } 5 | -------------------------------------------------------------------------------- /src/Components/LoadingAnimation.tsx: -------------------------------------------------------------------------------- 1 | import "./LoadingAnimation.css"; 2 | import * as React from "react"; 3 | 4 | export function LoadingAnimation() { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/Components/OutputCell.css: -------------------------------------------------------------------------------- 1 | .shinylive-output-cell div.rendered-html table { 2 | border: none; 3 | border-collapse: collapse; 4 | border-spacing: 0; 5 | color: black; 6 | font-size: 90%; 7 | table-layout: fixed; 8 | } 9 | 10 | .shinylive-output-cell div.rendered-html thead { 11 | border-bottom: 1px solid black; 12 | vertical-align: bottom; 13 | } 14 | 15 | .shinylive-output-cell div.rendered-html tr, 16 | .shinylive-output-cell div.rendered-html th, 17 | .shinylive-output-cell div.rendered-html td { 18 | text-align: right; 19 | vertical-align: middle; 20 | padding: 0.5em 0.5em; 21 | line-height: normal; 22 | white-space: normal; 23 | max-width: none; 24 | border: none; 25 | } 26 | 27 | .shinylive-output-cell div.rendered-html th { 28 | font-weight: bold; 29 | } 30 | 31 | .shinylive-output-cell div.rendered-html tbody tr:nth-child(odd) { 32 | background: #f5f5f5; 33 | } 34 | 35 | .shinylive-output-cell div.rendered-html tbody tr:hover { 36 | background: rgba(66, 165, 245, 0.2); 37 | } 38 | -------------------------------------------------------------------------------- /src/Components/OutputCell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { ToHtmlResult } from "../pyodide-proxy"; 3 | import type { ProxyHandle } from "./App"; 4 | import "./OutputCell.css"; 5 | import type { TerminalMethods } from "./Terminal"; 6 | 7 | // ============================================================================= 8 | // OutputCell component 9 | // ============================================================================= 10 | export function OutputCell({ 11 | proxyHandle, 12 | setTerminalMethods, 13 | }: { 14 | proxyHandle: ProxyHandle; 15 | setTerminalMethods: React.Dispatch>; 16 | }) { 17 | const [content, setContent] = React.useState({ 18 | type: "text", 19 | value: "", 20 | }); 21 | 22 | React.useEffect(() => { 23 | if (!proxyHandle.ready) return; 24 | if (proxyHandle.engine !== "pyodide") return; 25 | 26 | const runCodeInTerminal = async (command: string): Promise => { 27 | try { 28 | const result = await proxyHandle.pyodide.runPyAsync(command, { 29 | returnResult: "to_html", 30 | printResult: false, 31 | }); 32 | 33 | setContent(result); 34 | } catch (e) { 35 | setContent({ type: "text", value: (e as Error).message }); 36 | } 37 | }; 38 | 39 | setTerminalMethods({ 40 | ready: true, 41 | runCodeInTerminal, 42 | }); 43 | }, [setTerminalMethods, proxyHandle]); 44 | 45 | React.useEffect(() => { 46 | if (!proxyHandle.ready) return; 47 | if (proxyHandle.engine !== "webr") return; 48 | 49 | const runCodeInTerminal = async (command: string): Promise => { 50 | const shelter = await new proxyHandle.webRProxy.webR.Shelter(); 51 | try { 52 | const ret = await shelter.captureR(command, { 53 | withAutoprint: true, 54 | captureConditions: false, 55 | captureStreams: true, 56 | }); 57 | const output = ret.output as { type: string; data: string }[]; 58 | setContent({ 59 | type: "text", 60 | value: output 61 | .map((line: { type: string; data: string }) => line.data) 62 | .join("\n"), 63 | }); 64 | } catch (e) { 65 | setContent({ type: "text", value: (e as Error).message }); 66 | } finally { 67 | await shelter.purge(); 68 | } 69 | }; 70 | 71 | setTerminalMethods({ 72 | ready: true, 73 | runCodeInTerminal, 74 | }); 75 | }, [setTerminalMethods, proxyHandle]); 76 | 77 | return ( 78 |
79 | {content.type === "html" ? ( 80 |
84 | ) : ( 85 |
86 |           {content.value}
87 |         
88 | )} 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/Components/ResizableGrid/ResizableGrid.css: -------------------------------------------------------------------------------- 1 | .ResizableGrid { 2 | --pad: 2px; 3 | --panel-gap: 3px; 4 | --sizer-margin-offset: calc(-1 * var(--panel-gap)); 5 | /* How much does the handle expand to make it easier to grab? */ 6 | --expansion-scale: 3; 7 | height: 100%; 8 | width: 100%; 9 | min-height: 80px; 10 | min-width: 400px; 11 | display: grid; 12 | padding: 0; 13 | gap: var(--panel-gap); 14 | position: relative; 15 | } 16 | 17 | .ResizableGrid > * { 18 | /* By putting explicit min values on sizes of the items we stop them from 19 | blowing out the grid by staying too big. See 20 | https://css-tricks.com/preventing-a-grid-blowout/ for more details */ 21 | min-width: 0; 22 | min-height: 0; 23 | } 24 | 25 | div.ResizableGrid--col-sizer, 26 | div.ResizableGrid--row-sizer { 27 | opacity: 0; 28 | position: relative; 29 | /* 30 | Transformation back to default size is nice and slow to make it less 31 | confusing when the mouse slips slighlty off the grab handle and then when 32 | returning to the same place it is nowhere to be found. 33 | */ 34 | transition: transform 1s 0.5s; 35 | } 36 | 37 | .ResizableGrid--col-sizer { 38 | grid-row: 1/-1; 39 | width: var(--panel-gap); 40 | margin-left: var(--sizer-margin-offset); 41 | height: 100%; 42 | cursor: ew-resize; 43 | } 44 | 45 | .ResizableGrid--row-sizer { 46 | grid-column: 1/-1; 47 | height: var(--panel-gap); 48 | margin-top: var(--sizer-margin-offset); 49 | width: 100%; 50 | cursor: ns-resize; 51 | } 52 | 53 | .ResizableGrid--col-sizer:hover, 54 | .ResizableGrid--row-sizer:hover { 55 | z-index: 9999; 56 | /* Make the transition to larger instant */ 57 | transition: transform 0s; 58 | } 59 | .ResizableGrid--col-sizer:hover { 60 | transform: scaleX(var(--expansion-scale)); 61 | } 62 | .ResizableGrid--row-sizer:hover { 63 | transform: scaleY(var(--expansion-scale)); 64 | } 65 | 66 | div#size-detection-cell { 67 | width: 100%; 68 | height: 100%; 69 | 70 | /* One of these will get over-ridden by inline css */ 71 | grid-row: 1/-1; 72 | grid-column: 1/-1; 73 | } 74 | -------------------------------------------------------------------------------- /src/Components/ResizableGrid/ResizableGrid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { getHasRelativeUnits } from "./DragToResizeHelpers"; 3 | import "./ResizableGrid.css"; 4 | import { useDragToResizeGrid } from "./useDragToResizeGrid"; 5 | 6 | export function ResizableGrid({ 7 | className, 8 | children, 9 | areas, 10 | rowSizes, 11 | colSizes, 12 | style = {}, 13 | }: { 14 | className?: string; 15 | children?: React.ReactNode; 16 | areas: string[][]; 17 | rowSizes: string[]; 18 | colSizes: string[]; 19 | style?: React.CSSProperties; 20 | }) { 21 | const containerRef = React.useRef(null); 22 | const styleGrid = { 23 | gridTemplateAreas: areas.map((x) => `"${x.join(" ")}"`).join(" \n "), 24 | gridTemplateRows: rowSizes.join(" "), 25 | gridTemplateColumns: colSizes.join(" "), 26 | } as React.CSSProperties; 27 | 28 | // Build indices of the sizers needed. If there is only a single tract then no 29 | // resizers are needed. 30 | const hasRelativeRows = getHasRelativeUnits(rowSizes); 31 | 32 | const columnSizers = 33 | colSizes.length > 1 ? buildRange(2, colSizes.length) : []; 34 | const rowSizers = 35 | rowSizes.length > 1 36 | ? buildRange(2, rowSizes.length + (hasRelativeRows ? 0 : 1)) 37 | : []; 38 | 39 | const { startDrag } = useDragToResizeGrid({ 40 | containerRef, 41 | }); 42 | 43 | const classes = ["ResizableGrid"]; 44 | if (className) classes.push(className); 45 | 46 | return ( 47 |
52 | {columnSizers.map((gap_index) => ( 53 |
57 | startDrag({ e, dir: "columns", index: gap_index }) 58 | } 59 | style={{ 60 | gridColumn: gap_index, 61 | }} 62 | /> 63 | ))} 64 | {rowSizers.map((gap_index) => ( 65 |
startDrag({ e, dir: "rows", index: gap_index })} 68 | className="ResizableGrid--row-sizer" 69 | style={{ 70 | gridRow: gap_index, 71 | }} 72 | /> 73 | ))} 74 | {children} 75 |
76 | ); 77 | } 78 | 79 | function buildRange(from: number, to: number): number[] { 80 | const numEls = Math.abs(to - from) + 1; 81 | const step = from < to ? 1 : -1; 82 | return Array.from({ length: numEls }, (_, i) => from + i * step); 83 | } 84 | -------------------------------------------------------------------------------- /src/Components/ShareModal.css: -------------------------------------------------------------------------------- 1 | .ShareModal { 2 | position: absolute; 3 | top: 50px; 4 | left: 50px; 5 | width: 600px; 6 | font-family: var(--font-face); 7 | background: white; 8 | border: 1px solid #ccc; 9 | border-radius: 5px; 10 | padding: 8px 14px; 11 | transition: 0.4s ease-out; 12 | box-shadow: 0.5rem 0.5rem 2rem rgb(0 0 0 / 30%); 13 | z-index: 10; 14 | } 15 | 16 | .ShareModal .ShareModal--item { 17 | padding-bottom: 10px; 18 | } 19 | .ShareModal .ShareModal--item .ShareModal--checkbox { 20 | margin-left: 1em; 21 | font-size: 0.9em; 22 | } 23 | .ShareModal .ShareModal--row { 24 | display: flex; 25 | } 26 | 27 | .ShareModal .ShareModal--row label { 28 | display: flex; 29 | align-items: center; 30 | } 31 | 32 | .ShareModal .ShareModal--row label span { 33 | margin-left: 0.3em; 34 | } 35 | 36 | .ShareModal .ShareModal--row .ShareModal--url { 37 | display: inline-flex; 38 | position: relative; 39 | vertical-align: top; 40 | width: 100%; 41 | } 42 | 43 | .ShareModal .ShareModal--row .ShareModal--url .ShareModal--urlinput { 44 | font-family: var(--font-mono-face); 45 | font-size: var(--font-mono-size); 46 | width: 100%; 47 | margin-right: 12px; 48 | } 49 | 50 | .ShareModal-overlay { 51 | opacity: 0.5; 52 | background: #000; 53 | width: 100%; 54 | height: 100%; 55 | z-index: 9; 56 | top: 0; 57 | left: 0; 58 | position: fixed; 59 | } 60 | -------------------------------------------------------------------------------- /src/Components/Terminal.css: -------------------------------------------------------------------------------- 1 | /* 2 | This very-specific selector is needed to override some settings from App.css. 3 | */ 4 | .shinylive-terminal.terminal { 5 | height: 100%; 6 | position: relative; 7 | overflow: hidden; 8 | padding: 0.5rem; 9 | --color: #333; 10 | --background: white; 11 | --size: 1; 12 | --font: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 13 | } 14 | 15 | .xterm { 16 | padding: 8px; 17 | } 18 | -------------------------------------------------------------------------------- /src/Components/Viewer.css: -------------------------------------------------------------------------------- 1 | .shinylive-viewer { 2 | height: 100%; 3 | width: 100%; 4 | position: relative; 5 | } 6 | 7 | .app-frame { 8 | border-radius: var(--panel-roundness); 9 | border: 0; 10 | } 11 | 12 | .shinylive-viewer iframe { 13 | background-color: white; 14 | width: 100%; 15 | height: 100%; 16 | display: block; 17 | } 18 | 19 | .shinylive-viewer .loading-wrapper { 20 | overflow: auto; 21 | background-color: white; 22 | position: absolute; 23 | height: 100%; 24 | width: 100%; 25 | top: 0px; 26 | left: 0px; 27 | display: flex; 28 | justify-content: center; 29 | align-items: center; 30 | } 31 | 32 | .shinylive-viewer .loading-wrapper.loading-wrapper-error { 33 | justify-content: left; 34 | } 35 | 36 | .shinylive-viewer .loading-wrapper .error-alert { 37 | text-align: center; 38 | font-size: 1.4rem; 39 | font-family: var(--font-face); 40 | -webkit-animation: loading-animation-fade-in 0.5s; 41 | animation: loading-animation-fade-in 0.5s; 42 | } 43 | 44 | .shinylive-viewer .loading-wrapper .error-alert .error-icon { 45 | display: inline-block; 46 | text-align: center; 47 | width: 85px; 48 | margin-bottom: 0.5rem; 49 | } 50 | 51 | .shinylive-viewer .loading-wrapper .error-alert .error-log { 52 | font-size: 0.7rem; 53 | text-align: left; 54 | padding: 0.3rem; 55 | } 56 | 57 | @keyframes loading-animation-fade-in { 58 | 0% { 59 | opacity: 0; 60 | } 61 | 100% { 62 | opacity: 1; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/diagnostics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2021, Micro:bit Educational Foundation and contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import { setDiagnostics, type Diagnostic } from "@codemirror/lint"; 7 | import type { EditorState, Text, Transaction } from "@codemirror/state"; 8 | import type { EditorView } from "@codemirror/view"; 9 | import * as LSP from "vscode-languageserver-protocol"; 10 | import { positionToOffset } from "./positions"; 11 | 12 | /// An action associated with a diagnostic. 13 | export interface Action { 14 | /// The label to show to the user. Should be relatively short. 15 | name: string; 16 | /// The function to call when the user activates this action. Is 17 | /// given the diagnostic's _current_ position, which may have 18 | /// changed since the creation of the diagnostic due to editing. 19 | apply: (view: EditorView, from: number, to: number) => void; 20 | } 21 | 22 | const severityMapping = { 23 | [LSP.DiagnosticSeverity.Error]: "error", 24 | [LSP.DiagnosticSeverity.Warning]: "warning", 25 | [LSP.DiagnosticSeverity.Information]: "info", 26 | // [LSP.DiagnosticSeverity.Hint]: "hint", 27 | [LSP.DiagnosticSeverity.Hint]: "info", 28 | } as const; 29 | 30 | export const diagnosticsMapping = ( 31 | document: Text, 32 | lspDiagnostics: LSP.Diagnostic[], 33 | ): Diagnostic[] => 34 | lspDiagnostics 35 | .map(({ range, message, severity, tags }): Diagnostic | undefined => { 36 | const from = positionToOffset(document, range.start); 37 | const to = positionToOffset(document, range.end); 38 | // Skip if we can't map to the current document. 39 | if (from !== undefined && to !== undefined) { 40 | return { 41 | from, 42 | to, 43 | // Missing severity is client defined. Warn for now. 44 | severity: severityMapping[severity ?? LSP.DiagnosticSeverity.Warning], 45 | message, 46 | }; 47 | } 48 | return undefined; 49 | }) 50 | .filter((x): x is Diagnostic => Boolean(x)); 51 | 52 | /** 53 | * Given an EditorState object and a LSP.Diagnostic[] for that state's 54 | * document, generate and return a Transaction for that EditorState that 55 | * that has diagnostics added to it. 56 | */ 57 | export function diagnosticToTransaction( 58 | editorState: EditorState, 59 | lspDiagnostics: LSP.Diagnostic[], 60 | ): Transaction { 61 | const diagnostics = diagnosticsMapping(editorState.doc, lspDiagnostics); 62 | 63 | const diagnosticsTransaction = editorState.update( 64 | setDiagnostics(editorState, diagnostics), 65 | ); 66 | 67 | return diagnosticsTransaction; 68 | } 69 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/documentation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2021, Micro:bit Educational Foundation and contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import DOMPurify from "dompurify"; 7 | import { marked } from "marked"; 8 | import { MarkupContent } from "vscode-languageserver-types"; 9 | 10 | export const renderDocumentation = ( 11 | documentation: MarkupContent | string | undefined, 12 | ): HTMLElement => { 13 | if (!documentation) { 14 | documentation = "No documentation"; 15 | } 16 | const div = document.createElement("div"); 17 | div.className = "docstring"; 18 | if (MarkupContent.is(documentation) && documentation.kind === "markdown") { 19 | try { 20 | div.innerHTML = renderMarkdown(documentation.value).__html; 21 | return div; 22 | } catch (e) { 23 | // Fall through to simple text below. 24 | } 25 | } 26 | const fallbackContent = MarkupContent.is(documentation) 27 | ? documentation.value 28 | : documentation; 29 | 30 | const p = div.appendChild(document.createElement("p")); 31 | p.appendChild(new Text(fallbackContent)); 32 | return div; 33 | }; 34 | 35 | export interface SanitisedHtml { 36 | __html: string; 37 | } 38 | 39 | // Workaround to open links in a new tab. 40 | DOMPurify.addHook("afterSanitizeAttributes", function (node) { 41 | if (node.tagName === "A") { 42 | node.setAttribute("target", "_blank"); 43 | node.setAttribute("rel", "noopener"); 44 | } 45 | }); 46 | 47 | export const renderMarkdown = (markdown: string): SanitisedHtml => { 48 | const html = DOMPurify.sanitize( 49 | marked.parse(markdown, { gfm: true, headerIds: false, mangle: false }), 50 | ); 51 | return { 52 | __html: html, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/hover.ts: -------------------------------------------------------------------------------- 1 | import type { Extension } from "@codemirror/state"; 2 | import { hoverTooltip, type Tooltip } from "@codemirror/view"; 3 | import { 4 | HoverRequest, 5 | type HoverParams, 6 | type MarkupContent, 7 | } from "vscode-languageserver-protocol"; 8 | import { 9 | createUri, 10 | type LanguageServerClient, 11 | } from "../../../language-server/client"; 12 | import { renderDocumentation } from "./documentation"; 13 | import { offsetToPosition } from "./positions"; 14 | 15 | export function hover( 16 | lspClient: LanguageServerClient, 17 | filename: string, 18 | ): Extension { 19 | return createHoverTooltip(lspClient, filename); 20 | } 21 | 22 | function createHoverTooltip( 23 | client: LanguageServerClient, 24 | filename: string, 25 | ): Extension { 26 | const uri = createUri(filename); 27 | 28 | return hoverTooltip(async (view, pos, side): Promise => { 29 | offsetToPosition(view.state.doc, pos); 30 | 31 | const params: HoverParams = { 32 | textDocument: { uri }, 33 | position: offsetToPosition(view.state.doc, pos), 34 | }; 35 | 36 | const result = await client.connection.sendRequest( 37 | HoverRequest.type, 38 | params, 39 | ); 40 | if (result === null) return null; 41 | 42 | // console.log(result); 43 | 44 | return { 45 | pos: pos, 46 | above: true, 47 | create(view) { 48 | const dom = renderDocumentation(result?.contents as MarkupContent); 49 | // console.log(dom); 50 | return { dom }; 51 | }, 52 | }; 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/lsp-extension.ts: -------------------------------------------------------------------------------- 1 | import type { Extension } from "@codemirror/state"; 2 | import { EditorView, type ViewUpdate } from "@codemirror/view"; 3 | import type { TextDocumentContentChangeEvent } from "vscode-languageserver-protocol"; 4 | import type { LanguageServerClient } from "../../../language-server/client"; 5 | import { inferFiletype } from "../../../utils"; 6 | import { autocompletion } from "./autocompletion"; 7 | import { hover } from "./hover"; 8 | import { offsetToPosition } from "./positions"; 9 | import { signatureHelp } from "./signatureHelp"; 10 | 11 | export function languageServerExtensions( 12 | lspClient: LanguageServerClient, 13 | filename: string, 14 | ): Extension[] { 15 | if (inferFiletype(filename) !== "python") { 16 | return []; 17 | } 18 | 19 | return [ 20 | EditorView.updateListener.of((u: ViewUpdate) => { 21 | if (u.docChanged) { 22 | // Send content updates to the language server. The easy but slow way to 23 | // do this is to send the entire document each time. However, we do an 24 | // optimization, where we send just changes. We collect them in 25 | // `allChanges` when we call `.iterChanges()`. 26 | let changeEvent: TextDocumentContentChangeEvent | null = null; 27 | let nChanges = 0; // Will either be 1 or 2 at the end 28 | 29 | u.changes.iterChanges((fromA, toA, fromB, toB, inserted) => { 30 | if (nChanges >= 2) return; 31 | nChanges += 1; 32 | 33 | changeEvent = { 34 | range: { 35 | start: offsetToPosition(u.startState.doc, fromA), 36 | end: offsetToPosition(u.startState.doc, toA), 37 | }, 38 | text: inserted.sliceString(0), 39 | }; 40 | }); 41 | 42 | // If we got here, then changeEvent must be non-null. 43 | changeEvent = changeEvent as unknown as TextDocumentContentChangeEvent; 44 | 45 | if (nChanges === 1) { 46 | // If we had exactly one change, send it to the Language Server. 47 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 48 | lspClient.changeFile(filename, changeEvent); 49 | } else { 50 | // If we had more than one change (because of multiple cursors), don't 51 | // send the changes; send the entire document. This is a bit slower, 52 | // but necessary. The issue is that in CodeMirror, the changes events 53 | // are recorded simultaneously, so the start position of each change 54 | // is the position of each cursor at t0. However, the Language Server 55 | // expects the changes to be recordered in order, which means that it 56 | // expects the cursor positions at t0, t1, t2, etc. These two schemes 57 | // aren't compatible, so we'll just send the entire document. 58 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 59 | lspClient.changeFile(filename, { text: u.view.state.doc.toString() }); 60 | } 61 | } 62 | }), 63 | autocompletion(lspClient, filename), 64 | signatureHelp(lspClient, filename, true), 65 | hover(lspClient, filename), 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/names.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2021, Micro:bit Educational Foundation and contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | export const removeFullyQualifiedName = (fn: string): string => { 8 | const bracket = fn.indexOf("("); 9 | const before = fn.substring(0, bracket); 10 | const remainder = fn.substring(bracket); 11 | 12 | const parts = before.split("."); 13 | const name = parts[parts.length - 1]; 14 | return name + remainder; 15 | }; 16 | 17 | export const nameFromSignature = (fn: string): string => { 18 | const bracket = fn.indexOf("("); 19 | const before = fn.substring(0, bracket); 20 | return before; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/positions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2021, Micro:bit Educational Foundation and contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | import type { Text } from "@codemirror/state"; 7 | import type { Position, Range } from "vscode-languageserver-protocol"; 8 | 9 | // See https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#position 10 | 11 | export const positionToOffset = ( 12 | document: Text, 13 | position: Position, 14 | ): number | undefined => { 15 | if (position.line >= document.lines) { 16 | return undefined; 17 | } 18 | const offset = document.line(position.line + 1).from + position.character; 19 | if (offset > document.length) return; 20 | return offset; 21 | }; 22 | 23 | export const offsetToPosition = (document: Text, offset: number): Position => { 24 | const line = document.lineAt(offset); 25 | return { 26 | line: line.number - 1, 27 | character: offset - line.from, 28 | }; 29 | }; 30 | 31 | export const inRange = (range: Range, position: Position): boolean => 32 | !isBefore(position, range.start) && !isAfter(position, range.end); 33 | 34 | const isBefore = (p1: Position, p2: Position): boolean => 35 | p1.line < p2.line || (p1.line === p2.line && p1.character < p2.character); 36 | 37 | const isAfter = (p1: Position, p2: Position): boolean => 38 | p1.line > p2.line || (p1.line === p2.line && p1.character > p2.character); 39 | -------------------------------------------------------------------------------- /src/Components/codeMirror/language-server/regexp-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2021, Micro:bit Educational Foundation and contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | 7 | /** 8 | * Escape a regular expression. 9 | * 10 | * @param unescaped A string. 11 | * @returns A regular expression that matches the literal string. 12 | */ 13 | export const escapeRegExp = (unescaped: string) => { 14 | return unescaped.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 15 | }; 16 | -------------------------------------------------------------------------------- /src/Components/codeMirror/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Text } from "@codemirror/state"; 2 | import type { EditorView } from "@codemirror/view"; 3 | 4 | export type CursorPosition = { line: number; col: number }; 5 | 6 | export function offsetToPosition(cmDoc: Text, offset: number): CursorPosition { 7 | const line = cmDoc.lineAt(offset); 8 | return { line: line.number, col: offset - line.from }; 9 | } 10 | 11 | export function positionToOffset(cmDoc: Text, pos: CursorPosition): number { 12 | const line = cmDoc.line(pos.line); 13 | // Try go to the next computed position (line.from + pos.col), but don't go 14 | // past the end of the line (line.to). 15 | const newOffset = Math.min(line.from + pos.col, line.to); 16 | 17 | // If the new offset is beyond the end of the document, just go to the end. 18 | if (newOffset > cmDoc.length) { 19 | return cmDoc.length; 20 | } 21 | return newOffset; 22 | } 23 | 24 | export function getSelectedText(cmView: EditorView): string { 25 | const cmState = cmView.state; 26 | return cmState.sliceDoc( 27 | cmState.selection.main.from, 28 | cmState.selection.main.to, 29 | ); 30 | } 31 | 32 | export function getCurrentLineText(cmView: EditorView): string { 33 | const cmState = cmView.state; 34 | const offset = cmState.selection.main.head; 35 | const pos = offsetToPosition(cmState.doc, offset); 36 | const lineText = cmState.doc.line(pos.line).text; 37 | return lineText; 38 | } 39 | 40 | export function moveCursorToNextLine(cmView: EditorView): void { 41 | const cmState = cmView.state; 42 | const offset = cmState.selection.main.head; 43 | const pos = offsetToPosition(cmState.doc, offset); 44 | pos.line += 1; 45 | 46 | // Don't go past the bottom 47 | if (pos.line > cmState.doc.lines) { 48 | return; 49 | } 50 | 51 | const nextLineOffset = positionToOffset(cmState.doc, pos); 52 | cmView.dispatch({ selection: { anchor: nextLineOffset } }); 53 | } 54 | -------------------------------------------------------------------------------- /src/Components/filecontent.ts: -------------------------------------------------------------------------------- 1 | // Shape of file content from user input (i.e. examples.json, data structure 2 | // passed to App.runApp). If the type is "binary", then content is a 3 | import { stringToUint8Array, uint8ArrayToString } from "../utils"; 4 | 5 | // base64-encoded string representation of the data. 6 | export type FileContentJson = { 7 | name: string; 8 | content: string; 9 | type?: "text" | "binary"; 10 | }; 11 | 12 | // Completed data structure for internal use. This also represents binary data 13 | // as a Uint8Array. 14 | export type FileContent = 15 | | { 16 | name: string; 17 | content: string; 18 | type: "text"; 19 | } 20 | | { 21 | name: string; 22 | content: Uint8Array; 23 | type: "binary"; 24 | }; 25 | 26 | // Convert FileContentJson to FileContent. 27 | export function FCJSONtoFC(x: FileContentJson): FileContent { 28 | if (x.type === "binary") { 29 | return { 30 | name: x.name, 31 | content: stringToUint8Array(window.atob(x.content)), 32 | type: "binary", 33 | }; 34 | } else { 35 | return { 36 | name: x.name, 37 | content: x.content, 38 | type: "text", 39 | }; 40 | } 41 | } 42 | 43 | // Sometimes we don't know whether the input is a FileContent or a 44 | // FileContentJson. This function will take either tpye and convert it to a 45 | // FileContent. 46 | export function FCorFCJSONtoFC(x: FileContent | FileContentJson): FileContent { 47 | if (x.type === "binary") { 48 | if (typeof x.content === "string") { 49 | return FCJSONtoFC(x as FileContentJson); 50 | } else { 51 | return x as FileContent; 52 | } 53 | } else { 54 | return { 55 | name: x.name, 56 | content: x.content, 57 | type: "text", 58 | }; 59 | } 60 | } 61 | 62 | /** 63 | * Convert FileContent to FileContentJson. 64 | */ 65 | export function FCtoFCJSON(x: FileContent): FileContentJson { 66 | if (x.type === "binary") { 67 | return { 68 | name: x.name, 69 | content: window.btoa(uint8ArrayToString(x.content)), 70 | type: "binary", 71 | }; 72 | } else { 73 | return { 74 | name: x.name, 75 | content: x.content, 76 | // To save a bit of space, don't include type:"text". 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Components/gist.ts: -------------------------------------------------------------------------------- 1 | // Shape of file content from user input (i.e. examples.json, data structure 2 | // passed to App.runApp). If the type is "binary", then content is a 3 | import { isBinary, stringToUint8Array, uint8ArrayToString } from "../utils"; 4 | import type { FileContent } from "./filecontent"; 5 | 6 | export type GistApiResponse = { 7 | url: string; 8 | id: string; 9 | files: { 10 | [filename: string]: GistFile; 11 | }; 12 | public: boolean; 13 | created_at: string; 14 | updated_at: string; 15 | description: string; 16 | comments: 1; 17 | }; 18 | 19 | type GistFile = { 20 | filename: string; 21 | type: string; 22 | language: string; 23 | raw_url: string; 24 | size: number; 25 | truncated: boolean; 26 | // content is base64-encoded, even for text files, because we make the 27 | // request with Accept: "application/vnd.github.v3.base64". 28 | content: string; 29 | }; 30 | 31 | export async function fetchGist(id: string): Promise { 32 | const response = await fetch("https://api.github.com/gists/" + id, { 33 | headers: { 34 | Accept: "application/vnd.github.v3.base64", 35 | }, 36 | }); 37 | const gistData = (await response.json()) as GistApiResponse; 38 | return gistData; 39 | } 40 | 41 | export async function gistApiResponseToFileContents( 42 | gist: GistApiResponse, 43 | ): Promise { 44 | const result: FileContent[] = []; 45 | 46 | for (const filename in gist.files) { 47 | const gistFile = gist.files[filename]; 48 | 49 | let binary: boolean; 50 | let contentString: string = ""; 51 | let contentArray: Uint8Array = new Uint8Array(0); 52 | 53 | // Some Gist file entries are truncated. The API docs say it happens 54 | // when the files are over a megabyte. For these files, we need to fetch 55 | // the file directly, and we'll put the content in place. 56 | // https://docs.github.com/en/rest/gists/gists#truncation 57 | if (gistFile.truncated) { 58 | const reponse = await fetch(gistFile.raw_url); 59 | const contentBlob = await reponse.blob(); 60 | contentArray = new Uint8Array(await contentBlob.arrayBuffer()); 61 | // The gist API includes the 'type' field, but they are not always 62 | // helpful. 'type' can be "text/plain" for some binary files like sqlite 63 | // .db files. 64 | binary = isBinary(contentArray); 65 | if (!binary) { 66 | contentString = uint8ArrayToString(contentArray); 67 | } 68 | } else { 69 | contentString = window.atob(gistFile.content); 70 | binary = isBinary(contentString); 71 | if (binary) { 72 | contentArray = stringToUint8Array(contentString); 73 | } 74 | } 75 | 76 | if (binary) { 77 | result.push({ 78 | name: gistFile.filename, 79 | type: "binary", 80 | content: contentArray, 81 | }); 82 | } else { 83 | result.push({ 84 | name: gistFile.filename, 85 | type: "text", 86 | content: contentString, 87 | }); 88 | } 89 | } 90 | 91 | return result; 92 | } 93 | -------------------------------------------------------------------------------- /src/Components/skull.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 27 | 28 | 39 | 40 | 42 | 44 | 45 | 47 | 49 | 50 | -------------------------------------------------------------------------------- /src/Components/utils.ts: -------------------------------------------------------------------------------- 1 | export function asCssLengthUnit(value?: number | string): string | undefined { 2 | if (value === undefined) { 3 | return undefined; 4 | } 5 | 6 | if (typeof value === "string") { 7 | return value; 8 | } 9 | 10 | if (typeof value !== "number") { 11 | return undefined; 12 | } 13 | 14 | return `${value}px`; 15 | } 16 | 17 | export function minCssLengthUnit( 18 | x?: number | string, 19 | y?: number | string, 20 | ignoreAuto: boolean = true, 21 | ): string | undefined { 22 | x = asCssLengthUnit(x); 23 | y = asCssLengthUnit(y); 24 | 25 | if (ignoreAuto) { 26 | x = x === "auto" ? undefined : x; 27 | y = y === "auto" ? undefined : y; 28 | } 29 | 30 | if (x === undefined && y === undefined) { 31 | return undefined; 32 | } 33 | 34 | if (x && !y) { 35 | return x; 36 | } 37 | 38 | if (!x && y) { 39 | return y; 40 | } 41 | 42 | return `min(${x}, ${y})`; 43 | } 44 | -------------------------------------------------------------------------------- /src/assets/shinylive-inject-socket.txt: -------------------------------------------------------------------------------- 1 | // src/messageportwebsocket.ts 2 | var MessagePortWebSocket = class extends EventTarget { 3 | constructor(port) { 4 | super(); 5 | this.readyState = 0; 6 | this.addEventListener("open", (e) => { 7 | if (this.onopen) { 8 | this.onopen(e); 9 | } 10 | }); 11 | this.addEventListener("message", (e) => { 12 | if (this.onmessage) { 13 | this.onmessage(e); 14 | } 15 | }); 16 | this.addEventListener("error", (e) => { 17 | if (this.onerror) { 18 | this.onerror(e); 19 | } 20 | }); 21 | this.addEventListener("close", (e) => { 22 | if (this.onclose) { 23 | this.onclose(e); 24 | } 25 | }); 26 | this._port = port; 27 | port.addEventListener("message", this._onMessage.bind(this)); 28 | port.start(); 29 | } 30 | // Call on the server side of the connection, to tell the client that 31 | // the connection has been established. 32 | accept() { 33 | if (this.readyState !== 0) { 34 | return; 35 | } 36 | this.readyState = 1; 37 | this._port.postMessage({ type: "open" }); 38 | } 39 | send(data) { 40 | if (this.readyState === 0) { 41 | throw new DOMException( 42 | "Can't send messages while WebSocket is in CONNECTING state", 43 | "InvalidStateError" 44 | ); 45 | } 46 | if (this.readyState > 1) { 47 | return; 48 | } 49 | this._port.postMessage({ type: "message", value: { data } }); 50 | } 51 | close(code, reason) { 52 | if (this.readyState > 1) { 53 | return; 54 | } 55 | this.readyState = 2; 56 | this._port.postMessage({ type: "close", value: { code, reason } }); 57 | this.readyState = 3; 58 | this.dispatchEvent(new CloseEvent("close", { code, reason })); 59 | } 60 | _onMessage(e) { 61 | const event = e.data; 62 | switch (event.type) { 63 | case "open": 64 | if (this.readyState === 0) { 65 | this.readyState = 1; 66 | this.dispatchEvent(new Event("open")); 67 | return; 68 | } 69 | break; 70 | case "message": 71 | if (this.readyState === 1) { 72 | this.dispatchEvent(new MessageEvent("message", { ...event.value })); 73 | return; 74 | } 75 | break; 76 | case "close": 77 | if (this.readyState < 3) { 78 | this.readyState = 3; 79 | this.dispatchEvent(new CloseEvent("close", { ...event.value })); 80 | return; 81 | } 82 | break; 83 | } 84 | this._reportError( 85 | `Unexpected event '${event.type}' while in readyState ${this.readyState}`, 86 | 1002 87 | ); 88 | } 89 | _reportError(message, code) { 90 | this.dispatchEvent(new ErrorEvent("error", { message })); 91 | if (typeof code === "number") { 92 | this.close(code, message); 93 | } 94 | } 95 | }; 96 | 97 | // src/shinylive-inject-socket.ts 98 | window.Shiny.createSocket = function() { 99 | const channel = new MessageChannel(); 100 | window.parent.postMessage( 101 | { 102 | type: "openChannel", 103 | // Infer app name from path: "/foo/app_abc123/"" => "app_abc123" 104 | appName: window.location.pathname.replace( 105 | new RegExp(".*/([^/]+)/$"), 106 | "$1" 107 | ), 108 | path: "/websocket/" 109 | }, 110 | "*", 111 | [channel.port2] 112 | ); 113 | return new MessagePortWebSocket(channel.port1); 114 | }; 115 | -------------------------------------------------------------------------------- /src/awaitable-queue.ts: -------------------------------------------------------------------------------- 1 | // A queue with an async dequeue operation 2 | export class AwaitableQueue { 3 | _buffer: Array = []; 4 | _promise: Promise; 5 | _resolve: () => void; 6 | 7 | constructor() { 8 | // make TS compiler happy 9 | this._resolve = null as any; 10 | this._promise = null as any; 11 | 12 | // Actually initialize _promise and _resolve 13 | this._notifyAll(); 14 | } 15 | 16 | async _wait() { 17 | await this._promise; 18 | } 19 | 20 | _notifyAll() { 21 | if (this._resolve) { 22 | this._resolve(); 23 | } 24 | this._promise = new Promise((resolve) => (this._resolve = resolve)); 25 | } 26 | 27 | async dequeue(): Promise { 28 | // Must use a while-loop here, there might be multiple callers waiting to 29 | // deqeueue simultaneously 30 | while (this._buffer.length === 0) { 31 | await this._wait(); 32 | } 33 | return this._buffer.shift()!; 34 | } 35 | 36 | enqueue(x: T): void { 37 | this._buffer.push(x); 38 | this._notifyAll(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/examples.ts: -------------------------------------------------------------------------------- 1 | import type { AppEngine } from "./Components/App"; 2 | import type { FileContent, FileContentJson } from "./Components/filecontent"; 3 | import { FCJSONtoFC } from "./Components/filecontent"; 4 | 5 | export type ExampleItemJson = { 6 | title: string; 7 | about?: string; 8 | files: FileContentJson[]; 9 | }; 10 | 11 | // For examples/index.json 12 | export type ExampleCategoryIndexJson = { 13 | engine: string; 14 | examples: { 15 | category: string; 16 | apps: string[]; 17 | }[]; 18 | }; 19 | 20 | // For examples.json 21 | export type ExampleIndexJson = { 22 | engine: string; 23 | examples: ExampleCategoryJson[]; 24 | }; 25 | 26 | export type ExampleCategoryJson = { 27 | category: string; 28 | apps: ExampleItemJson[]; 29 | }; 30 | 31 | export type ExampleItem = { 32 | title: string; 33 | about: string | null; 34 | files: FileContent[]; 35 | }; 36 | 37 | export type ExampleCategory = { 38 | category: string; 39 | apps: ExampleItem[]; 40 | }; 41 | 42 | export type ExamplePosition = { 43 | categoryIndex: number; 44 | index: number; 45 | }; 46 | 47 | let exampleCategories: ExampleCategory[] | null = null; 48 | 49 | export async function getExampleCategories( 50 | engine: AppEngine, 51 | ): Promise { 52 | if (exampleCategories) { 53 | return exampleCategories; 54 | } 55 | 56 | const response = await fetch("../shinylive/examples.json"); 57 | const exampleIndexJson = (await response.json()) as ExampleIndexJson[]; 58 | 59 | const exampleCategoriesJson = exampleIndexJson.find( 60 | (value) => value.engine === engine, 61 | ); 62 | 63 | if (!exampleCategoriesJson) { 64 | throw new Error(`No examples found for app engine ${engine}`); 65 | } 66 | 67 | exampleCategories = exampleCategoriesJson.examples.map( 68 | exampleCategoryJsonToExampleCategory, 69 | ); 70 | 71 | return exampleCategories; 72 | } 73 | 74 | export function findExampleByTitle( 75 | title: string, 76 | exampleCategories: ExampleCategory[], 77 | ): ExamplePosition | null { 78 | if (title === "") return null; 79 | 80 | // Convert everything to lowercase to make matching easier when typing by hand 81 | title = title.toLowerCase(); 82 | for ( 83 | let categoryIndex = 0; 84 | categoryIndex < exampleCategories.length; 85 | categoryIndex++ 86 | ) { 87 | const examples = exampleCategories[categoryIndex].apps; 88 | for (let index = 0; index < examples.length; index++) { 89 | if (sanitizeTitleForUrl(examples[index].title) === title) { 90 | return { categoryIndex, index }; 91 | } 92 | } 93 | } 94 | 95 | // Failed to find example 96 | return null; 97 | } 98 | 99 | export function sanitizeTitleForUrl(title: string) { 100 | return title 101 | .toLowerCase() 102 | .replace(/[\s/]/g, "-") 103 | .replace(/[^a-z0-9-]/g, ""); 104 | } 105 | 106 | function exampleCategoryJsonToExampleCategory( 107 | x: ExampleCategoryJson, 108 | ): ExampleCategory { 109 | return { 110 | category: x.category, 111 | apps: x.apps.map(exampleItemJsonToExampleItem), 112 | }; 113 | } 114 | 115 | function exampleItemJsonToExampleItem(x: ExampleItemJson): ExampleItem { 116 | return { 117 | title: x.title, 118 | about: x.about || null, 119 | files: x.files.map(FCJSONtoFC), 120 | }; 121 | } 122 | -------------------------------------------------------------------------------- /src/fonts/SourceSansPro-Regular.otf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/shinylive/8acb41b0d4090308551e2f30dccb68fa6c9071a9/src/fonts/SourceSansPro-Regular.otf.woff2 -------------------------------------------------------------------------------- /src/hooks/useOnEscOrClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function useOnEscOrClickOutside( 4 | ref: React.RefObject, 5 | handler: (event: KeyboardEvent | PointerEvent) => void 6 | ) { 7 | React.useEffect(() => { 8 | const listener = (event: KeyboardEvent | PointerEvent) => { 9 | if (event instanceof KeyboardEvent) { 10 | if (event.key === "Escape") { 11 | handler(event); 12 | } 13 | } else if (event instanceof PointerEvent) { 14 | // Do nothing if clicking ref's element or descendent elements 15 | if (!ref.current || ref.current.contains(event.target as Node)) { 16 | return; 17 | } 18 | handler(event); 19 | } 20 | }; 21 | 22 | document.addEventListener("keydown", listener); 23 | document.addEventListener("pointerdown", listener); 24 | 25 | return () => { 26 | document.removeEventListener("keydown", listener); 27 | document.removeEventListener("pointerdown", listener); 28 | }; 29 | }, [ref, handler]); 30 | } 31 | -------------------------------------------------------------------------------- /src/hooks/useRunOnceOnMount.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /** 4 | * Executes a callback function only once when the component mounts. 5 | * 6 | * @param {Function} callback - The callback function to be executed on mount. 7 | */ 8 | export function useRunOnceOnMount(callback: () => void) { 9 | // This is needed to prevent the callback from being run twice -- in React 10 | // strict mode, a useEffect will run twice. 11 | const hasRun = React.useRef(false); 12 | 13 | React.useEffect(() => { 14 | if (!hasRun.current) { 15 | callback(); 16 | hasRun.current = true; 17 | } 18 | // eslint-disable-next-line react-hooks/exhaustive-deps 19 | }, []); 20 | } 21 | -------------------------------------------------------------------------------- /src/language-server/null-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractMessageReader, 3 | AbstractMessageWriter, 4 | createMessageConnection, 5 | } from "vscode-jsonrpc"; 6 | import { LanguageServerClient, createUri } from "./client"; 7 | 8 | let nullClient: NullLspClient | null = null; 9 | 10 | /** 11 | * This returns a NullClient object. If this is called multiple times, it 12 | * will return the same object each time. 13 | */ 14 | export function ensureNullClient(): NullLspClient { 15 | if (!nullClient) { 16 | nullClient = new NullLspClient(); 17 | } 18 | return nullClient; 19 | } 20 | 21 | export class NullMessageReader extends AbstractMessageReader { 22 | public listen() { 23 | return { dispose: () => {} }; 24 | } 25 | } 26 | 27 | export class NullMessageWriter extends AbstractMessageWriter { 28 | public async write() {} 29 | public end(): void {} 30 | } 31 | 32 | /** 33 | * A "null" LSP client that listens for messages but does nothing. 34 | */ 35 | export class NullLspClient extends LanguageServerClient { 36 | constructor() { 37 | const conn = createMessageConnection( 38 | new NullMessageReader(), 39 | new NullMessageWriter(), 40 | ); 41 | conn.listen(); 42 | super(conn, "en", createUri("")); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/language-server/pyright-client.ts: -------------------------------------------------------------------------------- 1 | import { createMessageConnection } from "vscode-jsonrpc"; 2 | import { 3 | BrowserMessageReader, 4 | BrowserMessageWriter, 5 | } from "vscode-jsonrpc/browser"; 6 | import type * as LSP from "vscode-languageserver-protocol"; 7 | import * as utils from "../utils"; 8 | import { currentScriptDir } from "../utils"; 9 | import { LanguageServerClient, createUri } from "./client"; 10 | 11 | const workerScriptName = "pyright.worker.js"; 12 | 13 | let pyrightClient: PyrightLspClient | null = null; 14 | 15 | /** 16 | * This returns a PyrightClient object. If this is called multiple times, it 17 | * will return the same object each time. 18 | */ 19 | export function ensurePyrightClient(): PyrightLspClient { 20 | if (!pyrightClient) { 21 | pyrightClient = new PyrightLspClient(); 22 | } 23 | return pyrightClient; 24 | } 25 | 26 | /** 27 | * The in-browser Pyright Language Server needs a few extra notification 28 | * messages over and above the standard Language Server Protocol. This class 29 | * sends those messages. 30 | */ 31 | export class PyrightLspClient extends LanguageServerClient { 32 | constructor() { 33 | const workerScript = 34 | utils.currentScriptDir() + `/pyright/${workerScriptName}`; 35 | 36 | const foreground = new Worker(workerScript, { 37 | name: "pyright-foreground", 38 | }); 39 | const connection = createMessageConnection( 40 | new BrowserMessageReader(foreground), 41 | new BrowserMessageWriter(foreground), 42 | ); 43 | // TODO: Add a way to shut down background thread 44 | const workers: Worker[] = [foreground]; 45 | connection.onDispose(() => { 46 | workers.forEach((w) => w.terminate()); 47 | }); 48 | 49 | connection.listen(); 50 | 51 | super(connection, "en", createUri("")); 52 | } 53 | 54 | public override async createFile( 55 | filename: string, 56 | content: string, 57 | ): Promise { 58 | const params: LSP.CreateFile = { 59 | uri: createUri(filename), 60 | kind: "create", 61 | }; 62 | await this.connection.sendNotification("$/createFile", params); 63 | await super.createFile(filename, content); 64 | } 65 | 66 | public override async deleteFile(filename: string): Promise { 67 | const params: LSP.DeleteFile = { 68 | uri: createUri(filename), 69 | kind: "delete", 70 | }; 71 | await this.connection.sendNotification("$/deleteFile", params); 72 | await super.deleteFile(filename); 73 | } 74 | 75 | /** 76 | * This uses fetch() instead of import() so that esbuild will not inline the 77 | * entire JSON file into the .js bundle. 78 | */ 79 | override async getInitializationOptions(): Promise { 80 | const response = await fetch( 81 | currentScriptDir() + "/pyright/typeshed.en.json", 82 | ); 83 | const typeshed = await response.json(); 84 | 85 | return { 86 | files: typeshed, 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/load-shinylive-sw.ts: -------------------------------------------------------------------------------- 1 | import { currentScriptDir, dirname } from "./utils"; 2 | 3 | const localhostNames = ["localhost", "127.0.0.1", "[::1]"]; 4 | 5 | if ( 6 | window.location.protocol !== "https:" && 7 | !localhostNames.includes(window.location.hostname) 8 | ) { 9 | const errorMessage = 10 | "Shinylive uses a Service Worker, which requires either a connection to localhost, or a connection via https."; 11 | document.body.innerText = errorMessage; 12 | throw Error(errorMessage); 13 | } 14 | 15 | // Figure out path to shinylive-sw.js. This can be provided with a tag: 16 | // 17 | // In that case, the path is relative to the current _page_, not this script. 18 | // 19 | // If that meta tag isn't present, assume shinylive-sw.js is in the parent of 20 | // this script's directory. 21 | let serviceWorkerDir: string; 22 | const shinyliveMetaTag = document.querySelector( 23 | 'meta[name="shinylive:serviceworker_dir"]' 24 | ); 25 | if (shinyliveMetaTag !== null) { 26 | serviceWorkerDir = (shinyliveMetaTag as HTMLMetaElement).content; 27 | } else { 28 | serviceWorkerDir = dirname(currentScriptDir()); 29 | } 30 | // Remove trailing slash, if present. 31 | serviceWorkerDir = serviceWorkerDir.replace(/\/$/, ""); 32 | 33 | const serviceWorkerPath = serviceWorkerDir + "/shinylive-sw.js"; 34 | 35 | // Start the service worker as soon as possible, to maximize the 36 | // resources it will be able to cache on the first run. 37 | if ("serviceWorker" in navigator) { 38 | navigator.serviceWorker 39 | .register(serviceWorkerPath, { type: "module" }) 40 | .then(() => console.log("Service Worker registered")) 41 | .catch(() => console.log("Service Worker registration failed")); 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 44 | navigator.serviceWorker.ready.then(() => { 45 | if (!navigator.serviceWorker.controller) { 46 | // For Shift+Reload case; navigator.serviceWorker.controller will 47 | // never appear until a regular (not Shift+Reload) page load. 48 | window.location.reload(); 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/lzstring-worker.ts: -------------------------------------------------------------------------------- 1 | // LZString in a Web Worker 2 | 3 | import LZString from "lz-string"; 4 | 5 | self.onmessage = function (e: MessageEvent): void { 6 | const msg = e.data as RequestMessage; 7 | const messagePort: ResponseMesssagePort = e.ports[0]; 8 | 9 | let result: string; 10 | 11 | if (msg.type === "encode") { 12 | result = LZString.compressToEncodedURIComponent(msg.value); 13 | } else if (msg.type === "decode") { 14 | result = LZString.decompressFromEncodedURIComponent(msg.value); 15 | } else { 16 | throw new Error(`Unknown request message type: ${(msg as any).type}`); 17 | } 18 | 19 | messagePort.postMessage({ value: result }); 20 | }; 21 | 22 | interface ResponseMesssagePort extends Omit { 23 | postMessage(msg: ResponseMessage): void; 24 | } 25 | 26 | export interface RequestMessageEncode { 27 | type: "encode"; 28 | value: string; 29 | } 30 | 31 | export interface RequestMessageDecode { 32 | type: "decode"; 33 | value: string; 34 | } 35 | 36 | export type RequestMessage = RequestMessageEncode | RequestMessageDecode; 37 | 38 | export interface ResponseMessage { 39 | value: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/postable-error.ts: -------------------------------------------------------------------------------- 1 | // Error objects which need to be sent via postMessage() are turned into one of 2 | // these objects. This is because on Safari, `structuredClone(e)` throws an 3 | // error, even for a bare Error object, as in `structuredClone(new Error())`. I 4 | // think this is bug in Safari because Errors should be cloneable. 5 | // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm 6 | // To work around this, instead of sending the Error object directly, we'll 7 | // convert it to this plain object and send that. 8 | // 9 | // If Safari fixes this issue, we'll be able to remove all of this stuff, and 10 | // simply post the Error object directly. 11 | export type PostableErrorObject = { 12 | message: string; 13 | name: string; 14 | stack?: string; 15 | }; 16 | 17 | export function errorToPostableErrorObject( 18 | e: Error | any 19 | ): PostableErrorObject { 20 | const errObj: PostableErrorObject = { 21 | message: "An unknown error occured", 22 | name: e.name, 23 | }; 24 | 25 | if (!(e instanceof Error)) { 26 | return errObj; 27 | } 28 | 29 | errObj.message = e.message; 30 | 31 | if (e.stack) { 32 | errObj.stack = e.stack; 33 | } 34 | 35 | return errObj; 36 | } 37 | 38 | export function postableErrorObjectToError( 39 | errObj: PostableErrorObject | any 40 | ): Error { 41 | // In the future, it's probably better to use `Object.hasOwn()` instead of 42 | // `in`, but at the time of this writing (2022-07), it is very new and we 43 | // can't expect all browsers to support it yet. 44 | if ("message" in errObj && "name" in errObj) { 45 | // This is a PostableErrorObject. 46 | const err = new Error(errObj.message); 47 | err.name = errObj.name; 48 | if (errObj.stack !== undefined) { 49 | err.stack = errObj.stack; 50 | } 51 | return err; 52 | } else { 53 | return new Error("An unknown error occured"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/pyright/PYRIGHT_LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Pyright - A static type checker for the Python language 4 | Copyright (c) Microsoft Corporation. All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE 23 | -------------------------------------------------------------------------------- /src/pyright/README.md: -------------------------------------------------------------------------------- 1 | This is a special build of Pyright that can run in a Web Worker in a browser. 2 | 3 | Built from sources at: https://github.com/posit-dev/pyright/ from branch `pyright-browser` 4 | Commit 4a9135964b680d0a2d5120f4b0910b9a4cffc807 5 | 6 | ## Local testing with a local copy of pyright 7 | 8 | In order to test a local copy of shinylive with a local copy of pyright, do the following: 9 | 10 | - Build shinylive 11 | - Build pyright, as described in https://github.com/posit-dev/pyright/blob/pyright-browser/THIS_FORK.md 12 | - In the shinylive directory, do the following (and change `/path/to/pyright` as appropriate): 13 | 14 | ``` 15 | cd build/shinylive/pyright 16 | rm pyright.worker.js pyright.worker.js.map 17 | ( 18 | PYRIGHTPATH=/path/to/pyright 19 | ln -s $PYRIGHTPATH/pyright.worker.js 20 | ln -s $PYRIGHTPATH/pyright.worker.js.map 21 | ln -s $PYRIGHTPATH/pyright-internal 22 | ln -s $PYRIGHTPATH/browser-pyright pyright 23 | ) 24 | ``` 25 | 26 | - Run `make serve` 27 | 28 | 29 | The .js symlink will make it so that the web browser will load the copy of pyright that is built from the repo, and .js.map and the directory symlinks will make it so the source .ts files will show in the browser's JS debugger. 30 | -------------------------------------------------------------------------------- /src/run-python-blocks.ts: -------------------------------------------------------------------------------- 1 | import type { AppEngine, AppMode } from "./Components/App"; 2 | import type { Component } from "./parse-codeblock"; 3 | import { parseCodeBlock } from "./parse-codeblock"; 4 | // @ts-expect-error: This import is _not_ bundled. It would be nice to be able 5 | // import type information from ./Components/App (which gets compiled to 6 | // shinylive.js), but I haven't figured out how to make it do that and do this 7 | // (non-bundled) import. 8 | import { runApp } from "./shinylive.js"; 9 | 10 | // Select all of the DOM elements that match the combined selector. It's 11 | // important that they're selected in the order they appear in the page, so that 12 | // we execute them in the correct order. 13 | const blocks: NodeListOf = document.querySelectorAll( 14 | ".shinylive-python, .shinylive-r" 15 | ); 16 | 17 | blocks.forEach((block) => { 18 | const container = document.createElement("div"); 19 | container.className = "shinylive-wrapper"; 20 | 21 | const engine: AppEngine = block.classList.contains("shinylive-r") 22 | ? "r" 23 | : "python"; 24 | 25 | // Copy over explicitly-set style properties. 26 | container.style.cssText = block.style.cssText; 27 | 28 | block.parentNode!.replaceChild(container, block); 29 | 30 | const { files, quartoArgs } = parseCodeBlock(block.innerText, engine); 31 | 32 | const appMode = convertComponentArrayToAppMode(quartoArgs.components); 33 | 34 | const opts = { startFiles: files, ...quartoArgs }; 35 | runApp(container, appMode, opts, engine); 36 | }); 37 | 38 | /** 39 | * Convert an array of components, like ["editor", "viewer"] to an AppMode 40 | * string. 41 | */ 42 | function convertComponentArrayToAppMode( 43 | components: Component[] | Component 44 | ): AppMode { 45 | if (typeof components === "string") { 46 | components = [components]; 47 | } 48 | const c_string = components.sort().join(","); 49 | 50 | if (c_string === "editor,viewer") { 51 | return "editor-viewer"; 52 | } else if (c_string === "editor,terminal,viewer") { 53 | return "editor-terminal-viewer"; 54 | } else if (c_string === "editor,terminal") { 55 | return "editor-terminal"; 56 | } else if (c_string === "cell,editor") { 57 | return "editor-cell"; 58 | } else if (c_string === "viewer") { 59 | return "viewer"; 60 | } else { 61 | throw new Error( 62 | "Unknown shinylive component combination: " + JSON.stringify(components) 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/scripts/codeblock-to-json.ts: -------------------------------------------------------------------------------- 1 | // This script is meant to be executed with Deno, not Node.js! 2 | // 3 | // It converts a file containing codeblock content, to a JSON file containing an 4 | // array of FileContent objects. Reads from stdin and writes to stdout. 5 | // 6 | // Usage: 7 | // deno run codeblock-to-json.ts 8 | // 9 | // Deno can run Typescript directly, but we are still running it through esbuild 10 | // to generate JS because we want to bundle other files into it. If we didn't 11 | // bundle the files, we would need to do some weird stuff with paths for the 12 | // import to work at run time, because the output path structure is different. 13 | import { readLines } from "https://deno.land/std/io/mod.ts"; 14 | 15 | import type { AppEngine } from "../Components/App"; 16 | import { parseCodeBlock } from "../parse-codeblock"; 17 | 18 | const { args } = Deno; 19 | 20 | const lines: string[] = []; 21 | for await (const line of readLines(Deno.stdin)) { 22 | lines.push(line); 23 | } 24 | // Default to python to support legacy codeblocks with an old version of shinylive quarto extension 25 | const engine: AppEngine = args.length > 0 && args[0] == "r" ? "r" : "python"; 26 | 27 | const content = parseCodeBlock(lines, engine); 28 | 29 | await Deno.stdout.write(new TextEncoder().encode(JSON.stringify(content))); 30 | -------------------------------------------------------------------------------- /src/shinylive-inject-socket.ts: -------------------------------------------------------------------------------- 1 | // Note that this file gets compiled to ./shinylive-inject-socket.txt. This is a 2 | // JavaScript file which in turn gets imported into the service worker as a 3 | // string, and gets served to Shinylive applications when they request that 4 | // file. 5 | // 6 | // The reason for doing this is so that only the shinylive-sw.js file needs to 7 | // live at the top level, instead of both that file and shinylive-sw.js. 8 | // 9 | // If you change this file (or its dependencies), then you may need to run the 10 | // build step twice: once to compile this file to the output .txt file, and then 11 | // one more time to have it be incorporated into shinylive-sw.js. 12 | import { MessagePortWebSocket } from "./messageportwebsocket"; 13 | 14 | export {}; 15 | 16 | // Create an object that looks like a WebSocket which Shiny.js will use 17 | // to communicate to the Python backend. 18 | (window as any).Shiny.createSocket = function () { 19 | const channel = new MessageChannel(); 20 | window.parent.postMessage( 21 | { 22 | type: "openChannel", 23 | // Infer app name from path: "/foo/app_abc123/"" => "app_abc123" 24 | appName: window.location.pathname.replace( 25 | new RegExp(".*/([^/]+)/$"), 26 | "$1" 27 | ), 28 | path: "/websocket/", 29 | }, 30 | "*", 31 | [channel.port2] 32 | ); 33 | return new MessagePortWebSocket(channel.port1); 34 | }; 35 | -------------------------------------------------------------------------------- /src/style-resets.css: -------------------------------------------------------------------------------- 1 | /* Taken from https://www.joshwcomeau.com/css/custom-css-reset/ */ 2 | 3 | /* 4 | 1. Use a more-intuitive box-sizing model. 5 | */ 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | /* 12 | 2. Remove default margin 13 | */ 14 | * { 15 | margin: 0; 16 | } 17 | /* 18 | 3. Allow percentage-based heights in the application 19 | */ 20 | html, 21 | body { 22 | height: 100%; 23 | } 24 | /* 25 | Typographic tweaks! 26 | 4. Add accessible line-height 27 | 5. Improve text rendering 28 | */ 29 | body { 30 | line-height: 1.5; 31 | -webkit-font-smoothing: antialiased; 32 | } 33 | /* 34 | 6. Improve media defaults 35 | */ 36 | img, 37 | picture, 38 | video, 39 | canvas, 40 | svg { 41 | display: block; 42 | max-width: 100%; 43 | } 44 | /* 45 | 7. Remove built-in form typography styles 46 | */ 47 | input, 48 | button, 49 | textarea, 50 | select { 51 | font: inherit; 52 | } 53 | /* 54 | 8. Avoid text overflows 55 | */ 56 | p, 57 | h1, 58 | h2, 59 | h3, 60 | h4, 61 | h5, 62 | h6 { 63 | overflow-wrap: break-word; 64 | } 65 | /* 66 | 9. Create a root stacking context 67 | */ 68 | #root, 69 | #__next { 70 | isolation: isolate; 71 | } 72 | -------------------------------------------------------------------------------- /src/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | // This seems to be necessary so that the TS compiler doesn't complain about 2 | // importing svg files. 3 | declare module "*.svg" { 4 | import type * as React from "react"; 5 | 6 | export const ReactComponent: React.FunctionComponent< 7 | React.SVGProps & { title?: string } 8 | >; 9 | 10 | const src: string; 11 | export default src; 12 | } 13 | 14 | // This is so the TS compiler doesn't complain about importing text files. 15 | declare module "*.txt" { 16 | const content: string; 17 | export default content; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "jsx": "react-jsx", 5 | "target": "es2020", 6 | "module": "es2020", 7 | "strict": true, 8 | "isolatedModules": true, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | "types": [], 14 | "forceConsistentCasingInFileNames": true 15 | }, 16 | "include": [ 17 | "src", 18 | "scripts" 19 | ], 20 | "exclude": [ 21 | "src/pyodide", 22 | "src/pyright", 23 | "**/*.test.tsx", 24 | "node_modules" 25 | ], 26 | "$schema": "https://json.schemastore.org/tsconfig", 27 | "display": "Recommended" 28 | } 29 | --------------------------------------------------------------------------------