├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── pytest.yaml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── examples ├── altair │ └── app.py ├── ipyleaflet │ └── app.py ├── ipywidgets │ └── app.py ├── outputs │ ├── README.md │ ├── app.py │ ├── flights.yaml │ └── requirements.txt ├── plotly │ ├── README.md │ ├── app.py │ └── requirements.txt ├── pydeck │ └── app.py └── superzip │ ├── README.md │ ├── app.py │ ├── ratelimit.py │ ├── requirements.txt │ ├── styles.css │ ├── superzip.csv │ └── utils.py ├── js ├── package.json ├── src │ ├── _input.ts │ ├── comm.ts │ ├── output.ts │ └── utils.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock ├── pyrightconfig.json ├── scripts └── static_download.R ├── setup.cfg ├── setup.py └── shinywidgets ├── __init__.py ├── _as_widget.py ├── _cdn.py ├── _comm.py ├── _dependencies.py ├── _output_widget.py ├── _render_widget.py ├── _render_widget_base.py ├── _serialization.py ├── _shinywidgets.py ├── _utils.py ├── py.typed └── static ├── 1e59d2330b4c6deb84b340635ed36249.ttf ├── 20fd1704ea223900efa9fd4e869efb08.woff2 ├── 8b43027f47b20503057dfbbaa9401fef.eot ├── c1e38fd9e0e74ba58f7a2b77ef29fdd3.svg ├── f691f37e57f04c152e2315ab7dbad881.woff ├── libembed-amd.js ├── node_modules_codemirror_mode_sync_recursive_js_.output.js ├── output.js ├── shinywidgets.css └── vendors-node_modules_codemirror_mode_apl_apl_js-node_modules_codemirror_mode_asciiarmor_ascii-26282f.output.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * shinywidgets version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yaml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["main", "rc-*"] 7 | pull_request: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.9", "3.10", "3.11", "3.12"] 17 | fail-fast: false 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Upgrade pip, install wheel 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install wheel 30 | 31 | - name: Install dev version of htmltools 32 | run: | 33 | pip install git+https://github.com/posit-dev/py-htmltools 34 | 35 | - name: Install dev version of shiny 36 | run: | 37 | pip install git+https://github.com/posit-dev/py-shiny 38 | 39 | - name: Install dependencies 40 | run: | 41 | pip install -e ".[dev,test]" 42 | 43 | - name: Install 44 | run: | 45 | make install 46 | 47 | - name: pyright 48 | run: | 49 | make pyright 50 | 51 | #- name: Run unit tests 52 | # run: | 53 | # make test 54 | 55 | deploy: 56 | name: "Deploy to PyPI" 57 | runs-on: ubuntu-latest 58 | if: github.event_name == 'release' 59 | needs: [build] 60 | steps: 61 | - uses: actions/checkout@v3 62 | - name: "Set up Python 3.12" 63 | uses: actions/setup-python@v4 64 | with: 65 | python-version: "3.12" 66 | - name: Install dependencies 67 | run: | 68 | python -m pip install --upgrade pip 69 | pip install -e ".[dev,test]" 70 | - name: "Build Package" 71 | run: | 72 | make dist 73 | 74 | # test deploy ---- 75 | - name: "Test Deploy to PyPI" 76 | uses: pypa/gh-action-pypi-publish@release/v1 77 | if: startsWith(github.event.release.name, 'TEST') 78 | with: 79 | user: __token__ 80 | password: ${{ secrets.PYPI_TEST_API_TOKEN }} 81 | repository_url: https://test.pypi.org/legacy/ 82 | 83 | ## prod deploy ---- 84 | - name: "Deploy to PyPI" 85 | uses: pypa/gh-action-pypi-publish@release/v1 86 | if: startsWith(github.event.release.name, 'shinywidgets') 87 | with: 88 | user: __token__ 89 | password: ${{ secrets.PYPI_API_TOKEN }} 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE settings 105 | .vscode/ 106 | .idea/ 107 | 108 | 109 | js/node_modules 110 | typings/ 111 | Untitled*.ipynb 112 | 113 | rsconnect-python/ 114 | 115 | # Deploy to Connect Cloud Button 116 | examples/*/README_FILES/* 117 | examples/*/README.html 118 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "python.formatting.provider": "black", 5 | "python.linting.flake8Enabled": true, 6 | "editor.tabSize": 2, 7 | "files.encoding": "utf8", 8 | "files.eol": "\n", 9 | "[python]": { 10 | "editor.formatOnSave": true, 11 | "editor.tabSize": 4, 12 | "editor.codeActionsOnSave": { 13 | "source.organizeImports": "explicit" 14 | }, 15 | }, 16 | "isort.args":["--profile", "black"], 17 | "editor.rulers": [ 18 | 88 19 | ], 20 | "files.exclude": { 21 | "**/__pycache__": true, 22 | "build/**": true 23 | }, 24 | "autoDocstring.guessTypes": false, 25 | "search.exclude": { 26 | "build/**": true 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log for shinywidgets 2 | 3 | All notable changes to shinywidgets will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.2] - 2025-05-21 9 | 10 | * Eliminate the possibility of a single `@render_widget` output from keeping a view of prior renders. (#196) 11 | 12 | ## [0.6.1] - 2025-05-21 13 | 14 | * Fixed an issue introduced by v0.6.0 where cleanup wasn't happening when it should be. (#195) 15 | 16 | ## [0.6.0] - 2025-05-19 17 | 18 | * Widgets initialized inside a `reactive.effect()` are no longer automatically removed when the effect invalidates. (#191) 19 | 20 | ## [0.5.2] - 2025-04-04 21 | 22 | * Constructing a widget inside of a `shiny.reactive.ExtendedTask()` no longer errors out. (#188) 23 | 24 | ## [0.5.1] - 2025-01-30 25 | 26 | * Fixes 'AttributeError: object has no attribute "_repr_mimebundle_"'. (#184) 27 | 28 | ## [0.5.0] - 2025-01-29 29 | 30 | * Updates to accomodate the new plotly v6.0.0 release. (#182) 31 | * Fixed an issue with plotly graphs sometimes not getting fully removed from the DOM. (#178) 32 | * Added `anywidget` as a package dependency since it's needed now for `altair` and `plotly` (and installing this packages won't necessarily install `anywidget`). (#183) 33 | * Fixed an issue with ipyleaflet erroring out when attempting to read the `.model_id` property of a closed widget object. (#179) 34 | * Fixed an issue where altair charts would sometimes render to a 0 height after being shown, hidden, and then shown again. (#180) 35 | 36 | ## [0.4.2] - 2024-12-18 37 | 38 | * Fixed an issue where `@render_widget` would sometimes incorrectly render a new widget without removing the old one. (#167) 39 | 40 | ## [0.4.1] - 2024-12-17 41 | 42 | * Fixed a Python 3.9 compatibility issue. 43 | 44 | ## [0.4.0] - 2024-12-16 45 | 46 | * Fixed a memory leak issue. (#167) 47 | 48 | ## [0.3.4] - 2024-10-29 49 | 50 | * Fixed an issue where widgets would sometimes fail to render in a Quarto document. (#159) 51 | * Fixed an issue where importing shinywidgets before a ipywidget implementation can sometimes error in a Shiny Express app. (#163) 52 | 53 | ## [0.3.3] - 2024-08-13 54 | 55 | * Fixed a bug with receiving binary data on the frontend, which gets [quak](https://github.com/manzt/quak) and [mosaic-widget](https://idl.uw.edu/mosaic/jupyter/) working with `@render_widget`. (#152) 56 | 57 | ## [0.3.2] - 2024-04-16 58 | 59 | * Fixed a bug with multiple altair outputs not working inside a `@shiny.render.ui` decorator. (#140) 60 | * `@render_widget` no longer errors out when giving a `altair.FacetChart` class. (#142) 61 | * `@render_widget` no longer fails to serialize `decimal.Decimal` objects. (#138) 62 | 63 | ## [0.3.1] - 2024-03-01 64 | 65 | * Widgets no longer have a "flash" of incorrect size when first rendered. (#133) 66 | * `@render_widget` now works properly with `Widget`s that aren't `DOMWidget`s (i.e., widgets that aren't meant to be displayed directly). As a result, you can now use `@render_widget` to gain a reference to the widget instance, and then use that reference to update the widget's value. (#133) 67 | 68 | ## [0.3.0] - 2024-01-25 69 | 70 | * The `@render_widget` decorator now attaches a `widget` (and `value`) attribute to the function it decorates. This allows for easier access to the widget instance (or value), and eliminates the need for `register_widget` (which is now soft deprecated). (#119) 71 | * Added decorators for notable packages that require coercion to the `Widget` class: `@render_altair`, `@render_bokeh`, `@render_plotly`, and `@render_pydeck`. Using these decorators (over `@render_widget`) helps with typing on the `widget` attribute. (#119) 72 | * The `.properties()` method on `altair.Chart` object now works as expected again. (#129) 73 | * Reduce default plot margins on plotly graphs. 74 | 75 | ## [0.2.4] - 2023-11-20 76 | 77 | * Fixed several issues with filling layout behavior introduced in 0.2.3. (#124, #125) 78 | * `reactive_read()` now throws a more informative error when attempting to read non-existing or non-trait attributes. (#120) 79 | 80 | ## [0.2.3] - 2023-11-13 81 | 82 | * Widgets now `fill` inside of a `fillable` container by default. For examples, see the [ipyleaflet](https://github.com/posit-dev/py-shinywidgets/blob/main/examples/ipyleaflet/app.py), [plotly](https://github.com/posit-dev/py-shinywidgets/blob/main/examples/plotly/app.py), or other [output](https://github.com/posit-dev/py-shinywidgets/blob/main/examples/outputs/app.py) examples. If this intelligent filling isn't desirable, either provide a `height` or `fillable=False` on `output_widget()`. (#115) 83 | * `as_widget()` uses the new `altair.JupyterChart()` to coerce `altair.Chart()` into a `ipywidgets.widgets.Widget` instance. (#113) 84 | 85 | ## [0.2.2] - 2023-10-31 86 | 87 | * `@render_widget` now builds on `shiny`'s `render.transformer` infrastructure, and as a result, it works more seamlessly in `shiny.express` mode. (#110) 88 | * Closed #104: Officially support for Python 3.7. 89 | 90 | ## [0.2.1] - 2023-05-15 91 | 92 | * Actually export `as_widget()` (it was mistakenly not exported in 0.2.0). 93 | 94 | ## [0.2.0] - 2023-04-13 95 | 96 | * Closed #43: Fixed an issue where widgets would sometimes not load in a dynamic UI context. (#91, #93) 97 | * Closed #14: Added a `bokeh_dependency()` function to simplify use of bokeh widgets. (#85) 98 | * Closed #89: Exported `as_widget()`, which helps to coerce objects into ipywidgets, and is especially helpful for creating ipywidget objects before passing to `register_widget()` (this way, the ipywidget can then be updated in-place and/or used as a reactive value (`reactive_read()`)). (#90) 99 | * Closed #94: New `SHINYWIDGETS_CDN` and `SHINYWIDGETS_CDN_ONLY` environment variables were added to more easily specify the CDN provider. Also, the default provider has changed from to (#95) 100 | * A warning is no longer issued (by default) when the path to a local widget extension is not found. This is because, if an internet connection is available, the widget assests are still loaded via CDN. To restore the previous behavior, set the `SHINYWIDGETS_EXTENSION_WARNING` environment variable to `"true"`. (#95) 101 | * Closed #86: Fixed an issue with `{ipyleaflet}` sometimes becoming unresponsive due to too many mouse move event messages being sent to the server. (#98) 102 | 103 | ## [0.1.6] - 2023-03-24 104 | 105 | * Closed #79: make shinywidgets compatible with ipywidgets 8.0.5. (#66) 106 | 107 | ## [0.1.5] - 2023-03-10 108 | 109 | * Stopped use of `_package_dir` function from `htmltools`. 110 | 111 | * Miscellaneous typing fixes and updates. 112 | 113 | ## [0.1.4] - 2022-12-12 114 | 115 | ### Bug fixes 116 | 117 | * Fixed installation problems on Python 3.7. (#68) 118 | 119 | 120 | ## [0.1.3] - 2022-12-08 121 | 122 | ### Bug fixes 123 | 124 | * Closed #65: get shinywidgets working with ipywidgets 8.0.3. (#66) 125 | 126 | 127 | ## [0.1.2] - 2022-07-27 128 | 129 | Initial release of shinywidgets 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Posit Software, 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 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-include shinywidgets/static * 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | 12 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | define PRINT_HELP_PYSCRIPT 14 | import re, sys 15 | 16 | for line in sys.stdin: 17 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 18 | if match: 19 | target, help = match.groups() 20 | print("%-20s %s" % (target, help)) 21 | endef 22 | export PRINT_HELP_PYSCRIPT 23 | 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | clean-build: ## remove build artifacts 32 | rm -fr build/ 33 | rm -fr dist/ 34 | rm -fr .eggs/ 35 | find . -name '*.egg-info' -exec rm -fr {} + 36 | find . -name '*.egg' -exec rm -f {} + 37 | 38 | clean-pyc: ## remove Python file artifacts 39 | find . -name '*.pyc' -exec rm -f {} + 40 | find . -name '*.pyo' -exec rm -f {} + 41 | find . -name '*~' -exec rm -f {} + 42 | find . -name '__pycache__' -exec rm -fr {} + 43 | 44 | clean-test: ## remove test and coverage artifacts 45 | rm -fr .tox/ 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | rm -fr .pytest_cache 49 | 50 | lint/flake8: ## check style with flake8 51 | flake8 shinywidgets tests 52 | lint/black: ## check style with black 53 | black --check shinywidgets tests 54 | 55 | lint: lint/flake8 lint/black ## check style 56 | 57 | test: ## run tests quickly with the default Python 58 | pytest 59 | 60 | test-all: ## run tests on every Python version with tox 61 | tox 62 | 63 | coverage: ## check code coverage quickly with the default Python 64 | coverage run --source shinywidgets -m pytest 65 | coverage report -m 66 | coverage html 67 | $(BROWSER) htmlcov/index.html 68 | 69 | docs: ## generate Sphinx HTML documentation, including API docs 70 | rm -f docs/shinywidgets.rst 71 | rm -f docs/modules.rst 72 | sphinx-apidoc -o docs/ shinywidgets 73 | $(MAKE) -C docs clean 74 | $(MAKE) -C docs html 75 | $(BROWSER) docs/_build/html/index.html 76 | 77 | servedocs: docs ## compile the docs watching for changes 78 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . 79 | 80 | release: dist ## package and upload a release 81 | twine upload dist/* 82 | 83 | dist: clean ## builds source and wheel package 84 | python setup.py sdist 85 | python setup.py bdist_wheel 86 | ls -l dist 87 | 88 | install: dist ## install the package to the active Python's site-packages 89 | pip uninstall -y shinywidgets 90 | python3 -m pip install dist/shinywidgets*.whl 91 | 92 | pyright: ## type check with pyright 93 | pyright --pythonversion=3.11 94 | 95 | check: pyright lint ## check code quality with pyright, flake8, black and isort 96 | echo "Checking code with black." 97 | black --check . 98 | echo "Sorting imports with isort." 99 | isort --check-only --diff . 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | shinywidgets 2 | ================ 3 | 4 | Render [ipywidgets](https://ipywidgets.readthedocs.io/en/stable/) inside a 5 | [Shiny](https://shiny.rstudio.com/py) (for Python) app. 6 | 7 | See the [Jupyter Widgets](https://shiny.posit.co/py/docs/jupyter-widgets.html) article on the Shiny for Python website for more details. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | pip install shinywidgets 13 | ``` 14 | 15 | ## Development 16 | 17 | If you want to do development on `{shinywidgets}`, run: 18 | 19 | ```sh 20 | pip install -e ".[dev,test]" 21 | cd js && yarn watch 22 | ``` 23 | -------------------------------------------------------------------------------- /examples/altair/app.py: -------------------------------------------------------------------------------- 1 | import altair as alt 2 | import shiny.express 3 | from shiny import render 4 | from vega_datasets import data 5 | 6 | from shinywidgets import reactive_read, render_altair 7 | 8 | 9 | # Output selection information (click on legend in the plot) 10 | @render.code 11 | def selection(): 12 | pt = reactive_read(jchart.widget.selections, "point") 13 | return "Selected point: " + str(pt) 14 | 15 | # Replicate JupyterChart interactivity 16 | # https://altair-viz.github.io/user_guide/jupyter_chart.html#point-selections 17 | @render_altair 18 | def jchart(): 19 | brush = alt.selection_point(name="point", encodings=["color"], bind="legend") 20 | return alt.Chart(data.cars()).mark_point().encode( 21 | x='Horsepower:Q', 22 | y='Miles_per_Gallon:Q', 23 | color=alt.condition(brush, 'Origin:N', alt.value('grey')), 24 | ).add_params(brush) 25 | -------------------------------------------------------------------------------- /examples/ipyleaflet/app.py: -------------------------------------------------------------------------------- 1 | import ipyleaflet as L 2 | from shiny import reactive, render, req 3 | from shiny.express import input, ui 4 | 5 | from shinywidgets import reactive_read, render_widget 6 | 7 | ui.page_opts(title="ipyleaflet demo") 8 | 9 | with ui.sidebar(): 10 | ui.input_slider("zoom", "Map zoom level", value=4, min=1, max=10) 11 | 12 | @render_widget 13 | def lmap(): 14 | return L.Map(center=(52, 360), zoom=4) 15 | 16 | # When the slider changes, update the map's zoom attribute 17 | @reactive.Effect 18 | def _(): 19 | lmap.widget.zoom = input.zoom() 20 | 21 | # When zooming directly on the map, update the slider's value 22 | @reactive.Effect 23 | def _(): 24 | zoom = reactive_read(lmap.widget, "zoom") 25 | ui.update_slider("zoom", value=zoom) 26 | 27 | 28 | with ui.card(fill=False): 29 | # Everytime the map's bounds change, update the output message 30 | @render.ui 31 | def map_bounds(): 32 | b = reactive_read(lmap.widget, "bounds") 33 | req(b) 34 | lat = [round(x) for x in [b[0][0], b[0][1]]] 35 | lon = [round(x) for x in [b[1][0], b[1][1]]] 36 | return f"The map bounds is currently {lat} / {lon}" 37 | -------------------------------------------------------------------------------- /examples/ipywidgets/app.py: -------------------------------------------------------------------------------- 1 | import shiny.express 2 | from ipywidgets import IntSlider 3 | from shiny import render 4 | 5 | from shinywidgets import reactive_read, render_widget 6 | 7 | 8 | @render_widget 9 | def slider(): 10 | return IntSlider( 11 | value=7, 12 | min=0, 13 | max=10, 14 | step=1, 15 | description="Test:", 16 | disabled=False, 17 | continuous_update=False, 18 | orientation="horizontal", 19 | readout=True, 20 | readout_format="d", 21 | ) 22 | 23 | @render.ui 24 | def slider_val(): 25 | val = reactive_read(slider.widget, "value") 26 | return f"The value of the slider is: {val}" 27 | -------------------------------------------------------------------------------- /examples/outputs/README.md: -------------------------------------------------------------------------------- 1 | ## Outputs app 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/outputs/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | from shiny import * 5 | 6 | from shinywidgets import * 7 | 8 | app_dir = Path(__file__).parent 9 | 10 | app_ui = ui.page_sidebar( 11 | ui.sidebar( 12 | ui.input_radio_buttons( 13 | "framework", 14 | "Choose a widget", 15 | [ 16 | "altair", 17 | "plotly", 18 | "ipyleaflet", 19 | "pydeck", 20 | "quak", 21 | "mosaic", 22 | "ipysigma", 23 | "bokeh", 24 | "bqplot", 25 | "ipychart", 26 | "ipywebrtc", 27 | # TODO: fix ipyvolume, qgrid 28 | ], 29 | selected="altair", 30 | ) 31 | ), 32 | bokeh_dependency(), 33 | ui.output_ui("figure", fill=True, fillable=True), 34 | title="Hello Jupyter Widgets in Shiny for Python", 35 | fillable=True, 36 | ) 37 | 38 | 39 | def server(input: Inputs, output: Outputs, session: Session): 40 | @output(id="figure") 41 | @render.ui 42 | def _(): 43 | return ui.card( 44 | ui.card_header(input.framework()), 45 | output_widget(input.framework()), 46 | full_screen=True, 47 | ) 48 | 49 | @output(id="altair") 50 | @render_widget 51 | def _(): 52 | import altair as alt 53 | from vega_datasets import data 54 | 55 | source = data.stocks() 56 | 57 | return ( 58 | alt.Chart(source) 59 | .transform_filter('datum.symbol==="GOOG"') 60 | .mark_area( 61 | tooltip=True, 62 | line={"color": "#0281CD"}, 63 | color=alt.Gradient( 64 | gradient="linear", 65 | stops=[ 66 | alt.GradientStop(color="white", offset=0), 67 | alt.GradientStop(color="#0281CD", offset=1), 68 | ], 69 | x1=1, 70 | x2=1, 71 | y1=1, 72 | y2=0, 73 | ), 74 | ) 75 | .encode(alt.X("date:T"), alt.Y("price:Q")) 76 | .properties(title={"text": ["Google's stock price over time"]}) 77 | ) 78 | 79 | @output(id="plotly") 80 | @render_widget 81 | def _(): 82 | import plotly.express as px 83 | 84 | return px.density_heatmap( 85 | px.data.tips(), 86 | x="total_bill", 87 | y="tip", 88 | marginal_x="histogram", 89 | marginal_y="histogram", 90 | ) 91 | 92 | @output(id="ipyleaflet") 93 | @render_widget 94 | def _(): 95 | from ipyleaflet import Map, Marker 96 | 97 | m = Map(center=(52.204793, 360.121558), zoom=4) 98 | m.add_layer(Marker(location=(52.204793, 360.121558))) 99 | return m 100 | 101 | @output(id="pydeck") 102 | @render_widget 103 | def _(): 104 | import pydeck as pdk 105 | 106 | UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv" 107 | 108 | layer = pdk.Layer( 109 | "HexagonLayer", # `type` positional argument is here 110 | UK_ACCIDENTS_DATA, 111 | get_position=["lng", "lat"], 112 | auto_highlight=True, 113 | elevation_scale=50, 114 | pickable=True, 115 | elevation_range=[0, 3000], 116 | extruded=True, 117 | coverage=1, 118 | ) 119 | 120 | # Set the viewport location 121 | view_state = pdk.ViewState( 122 | longitude=-1.415, 123 | latitude=52.2323, 124 | zoom=6, 125 | min_zoom=5, 126 | max_zoom=15, 127 | pitch=40.5, 128 | bearing=-27.36, 129 | ) 130 | 131 | # Combined all of it and render a viewport 132 | return pdk.Deck(layers=[layer], initial_view_state=view_state) 133 | 134 | @output(id="quak") 135 | @render_widget 136 | def _(): 137 | import polars as pl 138 | import quak 139 | 140 | df = pl.read_parquet( 141 | "https://github.com/uwdata/mosaic/raw/main/data/athletes.parquet" 142 | ) 143 | return quak.Widget(df) 144 | 145 | @output(id="mosaic") 146 | @render_widget 147 | def _(): 148 | import polars as pl 149 | import yaml 150 | from mosaic_widget import MosaicWidget 151 | 152 | flights = pl.read_parquet( 153 | "https://github.com/uwdata/mosaic/raw/main/data/flights-200k.parquet" 154 | ) 155 | 156 | # Load weather spec, remove data key to ensure load from Pandas 157 | with open(app_dir / "flights.yaml") as f: 158 | spec = yaml.safe_load(f) 159 | _ = spec.pop("data") 160 | 161 | return MosaicWidget(spec, data={"flights": flights}) 162 | 163 | @output(id="ipysigma") 164 | @render_widget 165 | def _(): 166 | import igraph as ig 167 | from ipysigma import Sigma 168 | 169 | g = ig.Graph.Famous("Zachary") 170 | return Sigma( 171 | g, 172 | node_size=g.degree, 173 | node_color=g.betweenness(), 174 | node_color_gradient="Viridis", 175 | ) 176 | 177 | @output(id="bokeh") 178 | @render_widget 179 | def _(): 180 | from bokeh.plotting import figure 181 | 182 | x = [1, 2, 3, 4, 5] 183 | y = [6, 7, 2, 4, 5] 184 | p = figure(title="Simple line example", x_axis_label="x", y_axis_label="y") 185 | p.line(x, y, legend_label="Temp.", line_width=2) 186 | return p 187 | 188 | @output(id="bqplot") 189 | @render_widget 190 | def _(): 191 | from bqplot import Axis, Bars, Figure, LinearScale, Lines, OrdinalScale 192 | 193 | size = 20 194 | x_data = np.arange(size) 195 | scales = {"x": OrdinalScale(), "y": LinearScale()} 196 | 197 | return Figure( 198 | title="API Example", 199 | legend_location="bottom-right", 200 | marks=[ 201 | Bars( 202 | x=x_data, 203 | y=np.random.randn(2, size), 204 | scales=scales, 205 | type="stacked", 206 | ), 207 | Lines( 208 | x=x_data, 209 | y=np.random.randn(size), 210 | scales=scales, 211 | stroke_width=3, 212 | colors=["red"], 213 | display_legend=True, 214 | labels=["Line chart"], 215 | ), 216 | ], 217 | axes=[ 218 | Axis(scale=scales["x"], grid_lines="solid", label="X"), 219 | Axis( 220 | scale=scales["y"], 221 | orientation="vertical", 222 | tick_format="0.2f", 223 | grid_lines="solid", 224 | label="Y", 225 | ), 226 | ], 227 | ) 228 | 229 | @output(id="ipychart") 230 | @render_widget 231 | def _(): 232 | from ipychart import Chart 233 | 234 | dataset = { 235 | "labels": [ 236 | "Data 1", 237 | "Data 2", 238 | "Data 3", 239 | "Data 4", 240 | "Data 5", 241 | "Data 6", 242 | "Data 7", 243 | "Data 8", 244 | ], 245 | "datasets": [{"data": [14, 22, 36, 48, 60, 90, 28, 12]}], 246 | } 247 | 248 | return Chart(data=dataset, kind="bar") 249 | 250 | @output(id="ipywebrtc") 251 | @render_widget 252 | def _(): 253 | from ipywebrtc import CameraStream 254 | 255 | return CameraStream( 256 | constraints={ 257 | "facing_mode": "user", 258 | "audio": False, 259 | "video": {"width": 640, "height": 480}, 260 | } 261 | ) 262 | 263 | 264 | app = App(app_ui, server) 265 | -------------------------------------------------------------------------------- /examples/outputs/flights.yaml: -------------------------------------------------------------------------------- 1 | meta: 2 | title: Cross-Filter Flights (200k) 3 | description: > 4 | Histograms showing arrival delay, departure time, and distance flown for over 200,000 flights. 5 | Select a histogram region to cross-filter the charts. 6 | Each plot uses an `intervalX` interactor to populate a shared Selection 7 | with `crossfilter` resolution. 8 | data: 9 | flights: { file: data/flights-200k.parquet } 10 | params: 11 | brush: { select: crossfilter } 12 | vconcat: 13 | - plot: 14 | - mark: rectY 15 | data: { from: flights, filterBy: $brush } 16 | x: { bin: delay } 17 | y: { count: } 18 | fill: steelblue 19 | inset: 0.5 20 | - select: intervalX 21 | as: $brush 22 | xDomain: Fixed 23 | yTickFormat: s 24 | width: 1200 25 | height: 250 26 | - plot: 27 | - mark: rectY 28 | data: { from: flights, filterBy: $brush } 29 | x: { bin: time } 30 | y: { count: } 31 | fill: steelblue 32 | inset: 0.5 33 | - select: intervalX 34 | as: $brush 35 | xDomain: Fixed 36 | yTickFormat: s 37 | width: 1200 38 | height: 250 39 | - plot: 40 | - mark: rectY 41 | data: { from: flights, filterBy: $brush } 42 | x: { bin: distance } 43 | y: { count: } 44 | fill: steelblue 45 | inset: 0.5 46 | - select: intervalX 47 | as: $brush 48 | xDomain: Fixed 49 | yTickFormat: s 50 | width: 1200 51 | height: 250 52 | -------------------------------------------------------------------------------- /examples/outputs/requirements.txt: -------------------------------------------------------------------------------- 1 | shiny 2 | shinywidgets 3 | ipywidgets 4 | numpy 5 | pandas 6 | vega_datasets 7 | bokeh 8 | jupyter_bokeh 9 | ipyleaflet 10 | pydeck==0.8.0 11 | altair 12 | plotly 13 | bqplot 14 | ipychart 15 | ipywebrtc 16 | vega 17 | quak 18 | mosaic-widget 19 | polars 20 | -------------------------------------------------------------------------------- /examples/plotly/README.md: -------------------------------------------------------------------------------- 1 | ## Plotly app 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/plotly/app.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import plotly.graph_objs as go 3 | from shiny import reactive 4 | from shiny.express import input, ui 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from shinywidgets import render_plotly 8 | 9 | # Generate some data and fit a linear regression 10 | n = 10000 11 | dat = np.random.RandomState(0).multivariate_normal([0, 0], [(1, 0.5), (0.5, 1)], n).T 12 | x = dat[0] 13 | y = dat[1] 14 | fit = LinearRegression().fit(x.reshape(-1, 1), dat[1]) 15 | xgrid = np.linspace(start=min(x), stop=max(x), num=30) 16 | 17 | ui.page_opts(title="Plotly demo", fillable=True) 18 | 19 | ui.input_checkbox("show_fit", "Show fitted line", value=True) 20 | 21 | @render_plotly 22 | def scatterplot(): 23 | return go.FigureWidget( 24 | data=[ 25 | go.Scattergl( 26 | x=x, 27 | y=y, 28 | mode="markers", 29 | marker=dict(color="rgba(0, 0, 0, 0.05)", size=5), 30 | ), 31 | go.Scattergl( 32 | x=xgrid, 33 | y=fit.intercept_ + fit.coef_[0] * xgrid, 34 | mode="lines", 35 | line=dict(color="red", width=2), 36 | ), 37 | ], 38 | layout={"showlegend": False}, 39 | ) 40 | 41 | 42 | @reactive.Effect 43 | def _(): 44 | scatterplot.widget.data[1].visible = input.show_fit() 45 | -------------------------------------------------------------------------------- /examples/plotly/requirements.txt: -------------------------------------------------------------------------------- 1 | shiny 2 | shinywidgets 3 | ipywidgets 4 | numpy 5 | pandas 6 | plotly 7 | scikit-learn 8 | -------------------------------------------------------------------------------- /examples/pydeck/app.py: -------------------------------------------------------------------------------- 1 | import pydeck as pdk 2 | from shiny import reactive 3 | from shiny.express import input, ui 4 | 5 | from shinywidgets import render_pydeck 6 | 7 | ui.input_slider("zoom", "Zoom", 0, 20, 6, step=1) 8 | 9 | @render_pydeck 10 | def deckmap(): 11 | UK_ACCIDENTS_DATA = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv" 12 | layer = pdk.Layer( 13 | "HexagonLayer", 14 | UK_ACCIDENTS_DATA, 15 | get_position=["lng", "lat"], 16 | auto_highlight=True, 17 | elevation_scale=50, 18 | pickable=True, 19 | elevation_range=[0, 3000], 20 | extruded=True, 21 | coverage=1, 22 | ) 23 | view_state = pdk.ViewState( 24 | longitude=-1.415, 25 | latitude=52.2323, 26 | zoom=6, 27 | min_zoom=5, 28 | max_zoom=15, 29 | pitch=40.5, 30 | bearing=-27.36, 31 | ) 32 | return pdk.Deck(layers=[layer], initial_view_state=view_state) 33 | 34 | @reactive.effect() 35 | def _(): 36 | deckmap.value.initial_view_state.zoom = input.zoom() 37 | deckmap.value.update() 38 | -------------------------------------------------------------------------------- /examples/superzip/README.md: -------------------------------------------------------------------------------- 1 | ## Data 2 | 3 | 4 | 5 | 6 | 7 | The `superzip.csv` is the result of [this script](https://github.com/rstudio/shinycoreci-apps/blob/main/apps/063-superzip-example/global.R) 8 | -------------------------------------------------------------------------------- /examples/superzip/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import ipyleaflet as leaf 4 | import ipywidgets 5 | import pandas as pd 6 | from faicons import icon_svg 7 | from ratelimit import debounce 8 | from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui 9 | from utils import col_numeric, create_map, density_plot, heatmap_gradient 10 | 11 | from shinywidgets import output_widget, reactive_read, render_plotly, render_widget 12 | 13 | # Load data 14 | app_dir = Path(__file__).parent 15 | allzips = pd.read_csv(app_dir / "superzip.csv").sample( 16 | n=10000, random_state=42 17 | ) 18 | 19 | # ------------------------------------------------------------------------ 20 | # Define user interface 21 | # ------------------------------------------------------------------------ 22 | 23 | vars = { 24 | "Score": "overall superzip score", 25 | "College": "% college educated", 26 | "Income": "median income", 27 | "Population": "population", 28 | } 29 | 30 | app_ui = ui.page_navbar( 31 | ui.nav_spacer(), 32 | ui.nav_panel( 33 | "Interactive map", 34 | ui.layout_sidebar( 35 | ui.sidebar( 36 | output_widget("density_score"), 37 | output_widget("density_college"), 38 | output_widget("density_income"), 39 | output_widget("density_pop"), 40 | position="right", 41 | width=300, 42 | title=ui.tooltip( 43 | ui.span( 44 | "Overall density vs in bounds", 45 | icon_svg("circle-info").add_class("ms-2"), 46 | ), 47 | "The density plots show how various metrics behave overall (black) in comparison to within view (green)." 48 | ) 49 | ), 50 | ui.card( 51 | ui.card_header( 52 | "Heatmap showing ", 53 | ui.input_select("variable", None, vars, width="auto"), 54 | class_="d-flex align-items-center gap-3" 55 | ), 56 | output_widget("map"), 57 | ui.card_footer( 58 | ui.markdown("Data compiled for _Coming Apart: The State of White America, 1960-2010_ by Charles Murray (Crown Forum, 2012).") 59 | ), 60 | full_screen=True 61 | ) 62 | ) 63 | ), 64 | ui.nav_panel( 65 | "Data explorer", 66 | ui.layout_columns( 67 | ui.output_ui("data_intro"), 68 | ui.output_data_frame("zips_data"), 69 | col_widths=[3, 9] 70 | ), 71 | ui.layout_columns( 72 | output_widget("table_map"), 73 | col_widths=[-2, 8, -2] 74 | ) 75 | ), 76 | fillable="Interactive map", 77 | id="navbar", 78 | header=ui.include_css(app_dir / "styles.css"), 79 | title=ui.popover( 80 | [ 81 | "Superzip explorer", 82 | icon_svg("circle-info").add_class("ms-2"), 83 | ], 84 | ui.markdown("'Super Zips' is a term [coined by Charles Murray](https://www.amazon.com/Coming-Apart-State-America-1960-2010/dp/030745343X) to describe the most prosperous, highly educated demographic clusters"), 85 | placement="right", 86 | ), 87 | window_title="Superzip explorer" 88 | ) 89 | 90 | 91 | # ------------------------------------------------------------------------ 92 | # Server logic 93 | # ------------------------------------------------------------------------ 94 | 95 | 96 | def server(input: Inputs, output: Outputs, session: Session): 97 | # ------------------------------------------------------------------------ 98 | # Main map logic 99 | # ------------------------------------------------------------------------ 100 | @render_widget 101 | def map(): 102 | return create_map() 103 | 104 | # Keeps track of whether we're showing markers (zoomed in) or heatmap (zoomed out) 105 | show_markers = reactive.value(False) 106 | 107 | # Switch from heatmap to markers when zoomed into 200 or fewer zips 108 | @reactive.effect 109 | def _(): 110 | nzips = zips_in_bounds().shape[0] 111 | show_markers.set(nzips < 200) 112 | 113 | # When the variable changes, either update marker colors or redraw the heatmap 114 | @reactive.effect 115 | @reactive.event(input.variable) 116 | def _(): 117 | zips = zips_in_bounds() 118 | if not show_markers(): 119 | remove_heatmap() 120 | map.widget.add_layer(layer_heatmap()) 121 | else: 122 | zip_colors = dict(zip(zips.Zipcode, zips_marker_color())) 123 | for x in map.widget.layers: 124 | if x.name.startswith("marker-"): 125 | zipcode = int(x.name.split("-")[1]) 126 | if zipcode in zip_colors: 127 | x.color = zip_colors[zipcode] 128 | 129 | # When bounds change, maybe add new markers 130 | @reactive.effect 131 | @reactive.event(lambda: zips_in_bounds()) 132 | def _(): 133 | if not show_markers(): 134 | return 135 | zips = zips_in_bounds() 136 | if zips.empty: 137 | return 138 | 139 | # Be careful not to create markers until we know we need to add it 140 | current_markers = set( 141 | [m.name for m in map.widget.layers if m.name.startswith("marker-")] 142 | ) 143 | zips["Color"] = zips_marker_color() 144 | for _, row in zips.iterrows(): 145 | if ("marker-" + str(row.Zipcode)) not in current_markers: 146 | map.widget.add_layer(create_marker(row, color=row.Color)) 147 | 148 | # Change from heatmap to markers: remove the heatmap and show markers 149 | # Change from markers to heatmap: hide the markers and add the heatmap 150 | @reactive.effect 151 | @reactive.event(show_markers) 152 | def _(): 153 | if show_markers(): 154 | map.widget.remove_layer(layer_heatmap()) 155 | else: 156 | map.widget.add_layer(layer_heatmap()) 157 | 158 | opacity = 0.6 if show_markers() else 0.0 159 | 160 | for x in map.widget.layers: 161 | if x.name.startswith("marker-"): 162 | x.fill_opacity = opacity 163 | x.opacity = opacity 164 | 165 | # For some reason, ipyleaflet updates bounds to an incorrect value 166 | # when the map is hidden, so only update the bounds when the map is visible 167 | current_bounds = reactive.value() 168 | 169 | @reactive.effect 170 | def _(): 171 | bb = reactive_read(map.widget, "bounds") 172 | if input.navbar() != "Interactive map": 173 | return 174 | with reactive.isolate(): 175 | current_bounds.set(bb) 176 | 177 | @debounce(0.3) 178 | @reactive.calc 179 | def zips_in_bounds(): 180 | bb = req(current_bounds()) 181 | 182 | lats = (bb[0][0], bb[1][0]) 183 | lons = (bb[0][1], bb[1][1]) 184 | return allzips[ 185 | (allzips.Lat >= lats[0]) 186 | & (allzips.Lat <= lats[1]) 187 | & (allzips.Long >= lons[0]) 188 | & (allzips.Long <= lons[1]) 189 | ] 190 | 191 | @reactive.calc 192 | def zips_marker_color(): 193 | vals = allzips[input.variable()] 194 | domain = (vals.min(), vals.max()) 195 | vals_in_bb = zips_in_bounds()[input.variable()] 196 | return col_numeric(domain)(vals_in_bb) 197 | 198 | @reactive.calc 199 | def layer_heatmap(): 200 | locs = allzips[["Lat", "Long", input.variable()]].to_numpy() 201 | return leaf.Heatmap( 202 | locations=locs.tolist(), 203 | name="heatmap", 204 | gradient=heatmap_gradient, 205 | ) 206 | 207 | def remove_heatmap(): 208 | for x in map.widget.layers: 209 | if x.name == "heatmap": 210 | map.widget.remove_layer(x) 211 | 212 | zip_selected = reactive.value(None) 213 | 214 | @render_plotly 215 | def density_score(): 216 | return density_plot( 217 | allzips, 218 | zips_in_bounds(), 219 | selected=zip_selected(), 220 | var="Score", 221 | title="Overall Score", 222 | showlegend=True, 223 | ) 224 | 225 | @render_plotly 226 | def density_income(): 227 | return density_plot( 228 | allzips, 229 | zips_in_bounds(), 230 | selected=zip_selected(), 231 | var="Income" 232 | ) 233 | 234 | @render_plotly 235 | def density_college(): 236 | return density_plot( 237 | allzips, 238 | zips_in_bounds(), 239 | selected=zip_selected(), 240 | var="College" 241 | ) 242 | 243 | @render_plotly 244 | def density_pop(): 245 | return density_plot( 246 | allzips, 247 | zips_in_bounds(), 248 | selected=zip_selected(), 249 | var="Population", 250 | title="log10(Population)", 251 | ) 252 | 253 | @render.ui 254 | def data_intro(): 255 | zips = zips_in_bounds() 256 | 257 | md = ui.markdown( 258 | f""" 259 | {zips.shape[0]} zip codes are currently within the map's viewport, and amongst them: 260 | 261 | * {100*zips.Superzip.mean():.1f}% are superzips 262 | * Mean income is ${zips.Income.mean():.0f}k 💰 263 | * Mean population is {zips.Population.mean():.0f} 👨🏽👩🏽👦🏽 264 | * Mean college educated is {zips.College.mean():.1f}% 🎓 265 | 266 | Use the filter controls on the table's columns to drill down further or 267 | click on a row to 268 | """, 269 | ) 270 | 271 | return ui.div(md, class_="my-3 lead") 272 | 273 | @render.data_frame 274 | def zips_data(): 275 | return render.DataGrid(zips_in_bounds(), selection_mode="row") 276 | 277 | @reactive.calc 278 | def selected_row(): 279 | sel = zips_data.input_cell_selection() 280 | print(sel) 281 | if not sel["rows"]: 282 | return pd.DataFrame() 283 | return zips_data.data().iloc[sel["rows"][0]] 284 | 285 | @render_widget 286 | def table_map(): 287 | #req(not selected_row().empty) 288 | 289 | return create_map() 290 | 291 | @reactive.effect 292 | @reactive.event(selected_row) 293 | def _(): 294 | if selected_row().empty: 295 | return 296 | for x in table_map.widget.layers: 297 | if x.name.startswith("marker"): 298 | table_map.widget.remove_layer(x) 299 | m = create_marker(selected_row()) 300 | table_map.widget.add_layer(m) 301 | 302 | # Utility function to create a marker 303 | def create_marker(row, **kwargs): 304 | m = leaf.CircleMarker( 305 | location=(row.Lat, row.Long), 306 | # Currently doesn't work with ipywidgets >8.0 307 | # https://github.com/posit-dev/py-shinywidgets/issues/101 308 | popup=ipywidgets.HTML( 309 | f""" 310 | {row.City}, {row.State} ({row.Zipcode})
311 | {row.Score:.1f} overall score
312 | {row.College:.1f}% college educated
313 | ${row.Income:.0f}k median income
314 | {row.Population} people
315 | """ 316 | ), 317 | name=f"marker-{row.Zipcode}", 318 | **kwargs, 319 | ) 320 | 321 | def _on_click(**kwargs): 322 | coords = kwargs["coordinates"] 323 | idx = (allzips.Lat == coords[0]) & (allzips.Long == coords[1]) 324 | zip_selected.set(allzips[idx]) 325 | 326 | m.on_click(_on_click) 327 | 328 | return m 329 | 330 | 331 | app = App(app_ui, server, static_assets=app_dir / "www") 332 | -------------------------------------------------------------------------------- /examples/superzip/ratelimit.py: -------------------------------------------------------------------------------- 1 | # From https://gist.github.com/jcheng5/427de09573816c4ce3a8c6ec1839e7c0 2 | import functools 3 | import time 4 | 5 | from shiny import reactive 6 | 7 | 8 | def debounce(delay_secs): 9 | def wrapper(f): 10 | when = reactive.Value(None) 11 | trigger = reactive.Value(0) 12 | 13 | @reactive.Calc 14 | def cached(): 15 | """ 16 | Just in case f isn't a reactive calc already, wrap it in one. This ensures 17 | that f() won't execute any more than it needs to. 18 | """ 19 | return f() 20 | 21 | @reactive.Effect(priority=102) 22 | def primer(): 23 | """ 24 | Whenever cached() is invalidated, set a new deadline for when to let 25 | downstream know--unless cached() invalidates again 26 | """ 27 | try: 28 | cached() 29 | except Exception: 30 | ... 31 | finally: 32 | when.set(time.time() + delay_secs) 33 | 34 | @reactive.Effect(priority=101) 35 | def timer(): 36 | """ 37 | Watches changes to the deadline and triggers downstream if it's expired; if 38 | not, use invalidate_later to wait the necessary time and then try again. 39 | """ 40 | deadline = when() 41 | if deadline is None: 42 | return 43 | time_left = deadline - time.time() 44 | if time_left <= 0: 45 | # The timer expired 46 | with reactive.isolate(): 47 | when.set(None) 48 | trigger.set(trigger() + 1) 49 | else: 50 | reactive.invalidate_later(time_left) 51 | 52 | @reactive.Calc 53 | @reactive.event(trigger, ignore_none=False) 54 | @functools.wraps(f) 55 | def debounced(): 56 | return cached() 57 | 58 | return debounced 59 | 60 | return wrapper 61 | 62 | 63 | def throttle(delay_secs): 64 | def wrapper(f): 65 | last_signaled = reactive.Value(None) 66 | last_triggered = reactive.Value(None) 67 | trigger = reactive.Value(0) 68 | 69 | @reactive.Calc 70 | def cached(): 71 | return f() 72 | 73 | @reactive.Effect(priority=102) 74 | def primer(): 75 | try: 76 | cached() 77 | except Exception: 78 | ... 79 | finally: 80 | last_signaled.set(time.time()) 81 | 82 | @reactive.Effect(priority=101) 83 | def timer(): 84 | if last_triggered() is not None and last_signaled() < last_triggered(): 85 | return 86 | 87 | now = time.time() 88 | if last_triggered() is None or (now - last_triggered()) >= delay_secs: 89 | last_triggered.set(now) 90 | with reactive.isolate(): 91 | trigger.set(trigger() + 1) 92 | else: 93 | reactive.invalidate_later(delay_secs - (now - last_triggered())) 94 | 95 | @reactive.Calc 96 | @reactive.event(trigger, ignore_none=False) 97 | @functools.wraps(f) 98 | def throttled(): 99 | return cached() 100 | 101 | return throttled 102 | 103 | return wrapper 104 | -------------------------------------------------------------------------------- /examples/superzip/requirements.txt: -------------------------------------------------------------------------------- 1 | ipywidgets==7.8.1 # ipyleaflet's popup is broken with >8.0 2 | ipyleaflet 3 | shiny @ git+https://github.com/posit-dev/py-shiny@main 4 | shinywidgets 5 | plotly 6 | matplotlib 7 | scipy 8 | pandas 9 | faicons 10 | -------------------------------------------------------------------------------- /examples/superzip/styles.css: -------------------------------------------------------------------------------- 1 | .sidebar .plotly { 2 | height: 200px; 3 | } 4 | 5 | /* FigureWidget doesn't support config?!? */ 6 | .plotly .modebar-container { 7 | display: none; 8 | } 9 | 10 | .card-body:has(.leaflet-container) { 11 | padding: 0; 12 | } 13 | 14 | .card-footer p { 15 | margin-bottom: 0; 16 | } 17 | 18 | .popover-body p { 19 | margin-bottom: 0; 20 | } 21 | -------------------------------------------------------------------------------- /examples/superzip/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Tuple 2 | 3 | import ipyleaflet as leaf 4 | import matplotlib as mpl 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | import pandas as pd 8 | import plotly.figure_factory as ff 9 | import plotly.graph_objs as go 10 | import shiny 11 | from ipyleaflet import basemaps 12 | 13 | 14 | def create_map(**kwargs): 15 | map = leaf.Map( 16 | center=(37.45, -88.85), 17 | zoom=4, 18 | scroll_wheel_zoom=True, 19 | attribution_control=False, 20 | **kwargs, 21 | ) 22 | map.add_layer(leaf.basemap_to_tiles(basemaps.CartoDB.DarkMatter)) 23 | search = leaf.SearchControl( 24 | url="https://nominatim.openstreetmap.org/search?format=json&q={s}", 25 | position="topleft", 26 | zoom=10, 27 | ) 28 | map.add(search) 29 | return map 30 | 31 | def density_plot( 32 | overall: pd.DataFrame, 33 | in_bounds: pd.DataFrame, 34 | var: str, 35 | selected: Optional[pd.DataFrame] = None, 36 | title: Optional[str] = None, 37 | showlegend: bool = False, 38 | ): 39 | shiny.req(not in_bounds.empty) 40 | 41 | dat = [overall[var], in_bounds[var]] 42 | if var == "Population": 43 | dat = [np.log10(x) for x in dat] 44 | 45 | # Create distplot with curve_type set to 'normal' 46 | fig = ff.create_distplot( 47 | dat, 48 | ["Overall", "In bounds"], 49 | colors=["black", "#6DCD59"], 50 | show_rug=False, 51 | show_hist=False, 52 | ) 53 | # Remove tick labels 54 | fig.update_layout( 55 | # hovermode="x", 56 | height=200, 57 | showlegend=showlegend, 58 | margin=dict(l=0, r=0, t=0, b=0), 59 | legend=dict(x=0.5, y=1, orientation="h", xanchor="center", yanchor="bottom"), 60 | xaxis=dict( 61 | title=title if title is not None else var, 62 | showline=False, 63 | zeroline=False, 64 | ), 65 | yaxis=dict( 66 | showgrid=False, 67 | showline=False, 68 | showticklabels=False, 69 | zeroline=False, 70 | ), 71 | template="plotly_white" 72 | ) 73 | # hovermode itsn't working properly when dynamically, absolutely positioned 74 | for _, trace in enumerate(fig.data): 75 | trace.update(hoverinfo="none") 76 | 77 | if selected is not None: 78 | x = selected[var].tolist()[0] 79 | if var == "Population": 80 | x = np.log10(x) 81 | fig.add_shape( 82 | type="line", 83 | x0=x, 84 | x1=x, 85 | y0=0, 86 | y1=1, 87 | yref="paper", 88 | line=dict(width=1, dash="dashdot", color="gray"), 89 | ) 90 | 91 | return go.FigureWidget(data=fig.data, layout=fig.layout) 92 | 93 | 94 | color_palette = plt.get_cmap("viridis", 10) 95 | 96 | # TODO: how to handle nas (pd.isna)? 97 | def col_numeric(domain: Tuple[float, float], na_color: str = "#808080"): 98 | rescale = mpl.colors.Normalize(domain[0], domain[1]) 99 | 100 | def _(vals: List[float]) -> List[str]: 101 | cols = color_palette(rescale(vals)) 102 | return [mpl.colors.to_hex(v) for v in cols] 103 | 104 | return _ 105 | 106 | 107 | # R> cat(paste0(round(scales::rescale(log10(1:10), to = c(0.05, 1)), 2), ": '", viridis::viridis(10), "'"), sep = "\n") 108 | heatmap_gradient = { 109 | 0.05: "#440154", 110 | 0.34: "#482878", 111 | 0.5: "#3E4A89", 112 | 0.62: "#31688E", 113 | 0.71: "#26828E", 114 | 0.79: "#1F9E89", 115 | 0.85: "#35B779", 116 | 0.91: "#6DCD59", 117 | 0.96: "#B4DE2C", 118 | 1: "#FDE725", 119 | } 120 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter-widgets/shiny-embed-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "Shiny input/output bindings for ipywidgets", 6 | "license": "MIT", 7 | "author": "Carson Sievert", 8 | "main": "dist/index.js", 9 | "scripts": { 10 | "build": "webpack", 11 | "build_old": "esbuild src/index.ts --outfile=dist/index.js --format=iife --bundle --sourcemap=inline --target=es2020 --loader:.css=dataurl --loader:.svg=text", 12 | "watch": "yarn build --watch", 13 | "clean": "rimraf dist", 14 | "test": "npm run test:default", 15 | "test:default": "echo \"No test specified\"" 16 | }, 17 | "dependencies": { 18 | "@jupyter-widgets/html-manager": "^0.20.1", 19 | "@types/rstudio-shiny": "https://github.com/rstudio/shiny#main", 20 | "base64-arraybuffer": "^1.0.2", 21 | "font-awesome": "^4.7.0" 22 | }, 23 | "devDependencies": { 24 | "css-loader": "^5.2.6", 25 | "file-loader": "^6.2.0", 26 | "filemanager-webpack-plugin": "^6.1.7", 27 | "style-loader": "^2.0.0", 28 | "ts-loader": "^9.2.6", 29 | "ts-node": "^10.4.0", 30 | "typescript": "^4.5.2", 31 | "url-loader": "^4.1.1", 32 | "webpack": "^5.38.1", 33 | "webpack-cli": "^4.9.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /js/src/_input.ts: -------------------------------------------------------------------------------- 1 | import { HTMLManager, requireLoader } from '@jupyter-widgets/html-manager'; 2 | // N.B. for this to work properly, it seems we must include 3 | // https://unpkg.com/@jupyter-widgets/html-manager@*/dist/libembed-amd.js 4 | // on the page first, which is why that comes in as a 5 | import { renderWidgets } from '@jupyter-widgets/html-manager/lib/libembed'; 6 | import { RatePolicyModes } from 'rstudio-shiny/srcts/types/src/inputPolicies/inputRateDecorator'; 7 | 8 | 9 | // Render the widgets on page load, so that once the Shiny app initializes, 10 | // it has a chance to bind to the rendered widgets. 11 | window.addEventListener("load", () => { 12 | renderWidgets(() => new InputManager({ loader: requireLoader })); 13 | }); 14 | 15 | 16 | // Let the world know about a value change so the Shiny input binding 17 | // can subscribe to it (and thus call getValue() whenever that happens) 18 | class InputManager extends HTMLManager { 19 | display_view(msg, view, options): ReturnType { 20 | 21 | return super.display_view(msg, view, options).then((view) => { 22 | 23 | // Get the Shiny input container element for this view 24 | const $el_input = view.$el.parents(INPUT_SELECTOR); 25 | 26 | // At least currently, ipywidgets have a tagify method, meaning they can 27 | // be directly statically rendered (i.e., without a input_ipywidget() container) 28 | if ($el_input.length == 0) { 29 | return; 30 | } 31 | 32 | // Most "input-like" widgets use the value property to encode their current value, 33 | // but some multiple selection widgets (e.g., RadioButtons) use the index property 34 | // instead. 35 | let val = view.model.get("value"); 36 | if (val === undefined) { 37 | val = view.model.get("index"); 38 | } 39 | 40 | // Checkbox() apparently doesn't have a value/index property 41 | // on the model on the initial render (but does in the change event, 42 | // so this seems like an ipywidgets bug???) 43 | if (val === undefined && view.hasOwnProperty("checkbox")) { 44 | val = view.checkbox.checked; 45 | } 46 | 47 | // Button() doesn't have a value/index property, and clicking it doesn't trigger 48 | // a change event, so we do that ourselves 49 | if (val === undefined && view.tagName === "button") { 50 | val = 0; 51 | view.$el[0].addEventListener("click", () => { 52 | val++; 53 | _doChangeEvent($el_input[0], val); 54 | }); 55 | } 56 | 57 | // Mock a change event now so that we know Shiny binding has a chance to 58 | // read the initial value. Also, do it on the next tick since the 59 | // binding hasn't had a chance to subscribe to the change event yet. 60 | setTimeout(() => { _doChangeEvent($el_input[0], val) }, 0); 61 | 62 | // Relay changes to the model to the Shiny input binding 63 | view.model.on('change', (x) => { 64 | let val; 65 | if (x.attributes.hasOwnProperty("value")) { 66 | val = x.attributes.value; 67 | } else if (x.attributes.hasOwnProperty("index")) { 68 | val = x.attributes.index; 69 | } else { 70 | throw new Error("Unknown change event" + JSON.stringify(x.attributes)); 71 | } 72 | _doChangeEvent($el_input[0], val); 73 | }); 74 | 75 | }); 76 | 77 | } 78 | } 79 | 80 | function _doChangeEvent(el: HTMLElement, val: any): void { 81 | const evt = new CustomEvent(CHANGE_EVENT_NAME, { detail: val }); 82 | el.dispatchEvent(evt); 83 | } 84 | 85 | class IPyWidgetInput extends Shiny.InputBinding { 86 | find(scope: HTMLElement): JQuery { 87 | return $(scope).find(INPUT_SELECTOR); 88 | } 89 | getValue(el: HTMLElement): any { 90 | return $(el).data("value"); 91 | } 92 | setValue(el: HTMLElement, value: any): void { 93 | $(el).data("value", value); 94 | } 95 | subscribe(el: HTMLElement, callback: (x: boolean) => void): void { 96 | this._eventListener = (e: CustomEvent) => { 97 | this.setValue(el, e.detail); 98 | callback(true); 99 | }; 100 | el.addEventListener(CHANGE_EVENT_NAME, this._eventListener); 101 | } 102 | _eventListener; 103 | unsubscribe(el: HTMLElement): void { 104 | el.removeEventListener(CHANGE_EVENT_NAME, this._eventListener); 105 | } 106 | getRatePolicy(el: HTMLElement): { policy: RatePolicyModes; delay: number } { 107 | const policy = el.attributes.getNamedItem('data-rate-policy'); 108 | const delay = el.attributes.getNamedItem('data-rate-delay'); 109 | return { 110 | // @ts-ignore: python typing ensures this is a valid policy 111 | policy: policy ? policy.value : 'debounce', 112 | delay: delay ? parseInt(delay.value) : 250 113 | } 114 | } 115 | // TODO: implement receiveMessage? 116 | } 117 | 118 | Shiny.inputBindings.register(new IPyWidgetInput(), "shiny.IPyWidgetInput"); 119 | 120 | 121 | const INPUT_SELECTOR = ".shiny-ipywidget-input"; 122 | const CHANGE_EVENT_NAME = 'IPyWidgetInputValueChange' 123 | 124 | 125 | 126 | 127 | // // Each widget has multiple "models", and each model has a state. 128 | // // For an input widget, it seems reasonable to assume there is only one model 129 | // // state that contains the value/index, so we search the collection of model 130 | // // states for one with a value property, and return that (otherwise, error) 131 | // function _getValue(states: object): any { 132 | // const vals = []; 133 | // Object.entries(states).forEach(([key, val]) => { 134 | // if (val.state.hasOwnProperty('value')) { 135 | // vals.push(val.state.value); 136 | // } else if (val.state.hasOwnProperty('index')) { 137 | // vals.push(val.state.index); 138 | // } 139 | // }); 140 | // if (vals.length > 1) { 141 | // throw new Error("Expected there to be exactly one model state with a value property, but found: " + vals.length); 142 | // } 143 | // return vals[0]; 144 | // } 145 | // 146 | // function _getStates(el: HTMLElement): object { 147 | // const el_state = el.querySelector('script[type="application/vnd.jupyter.widget-state+json"]'); 148 | // return JSON.parse(el_state.textContent).state; 149 | // } 150 | 151 | // function set_state(el) { 152 | // let states = _getStates(el); 153 | // Object.entries(states).forEach(([key, val]) => { 154 | // if (val.state && val.state.hasOwnProperty('value')) { 155 | // states[key].state.value = value; 156 | // } 157 | // }); 158 | // let el_state = el.querySelector(WIDGET_STATE_SELECTOR); 159 | // el_state.textContent = JSON.stringify(states); 160 | // // Re-render the widget with the new state 161 | // // Unfortunately renderWidgets() doesn't clear out the div.widget-subarea, 162 | // // so repeated calls to renderWidgets() will render multiple views 163 | // el.querySelector('.widget-subarea').remove(); 164 | // renderWidgets(() => new InputManager({ loader: requireLoader }), el); 165 | // } -------------------------------------------------------------------------------- /js/src/comm.ts: -------------------------------------------------------------------------------- 1 | import { Throttler } from "./utils"; 2 | 3 | // This class is a striped down version of Comm from @jupyter-widgets/base 4 | // https://github.com/jupyter-widgets/ipywidgets/blob/88cec8/packages/base/src/services-shim.ts#L192-L335 5 | // Note that the Kernel.IComm implementation is located here 6 | // https://github.com/jupyterlab/jupyterlab/blob/master/packages/services/src/kernel/comm.ts 7 | export class ShinyComm { 8 | 9 | // It seems like we'll want one comm per model 10 | comm_id: string; 11 | throttler: Throttler; 12 | constructor(model_id: string) { 13 | this.comm_id = model_id; 14 | // TODO: make this configurable (see comments in send() below)? 15 | this.throttler = new Throttler(100); 16 | } 17 | 18 | // This might not be needed 19 | get target_name(): string { 20 | return "jupyter.widgets"; 21 | } 22 | 23 | _msg_callback; 24 | _close_callback; 25 | 26 | send( 27 | data: any, 28 | callbacks: any, 29 | metadata?: any, 30 | buffers?: ArrayBuffer[] | ArrayBufferView[] 31 | ): string { 32 | const msg = { 33 | content: {comm_id: this.comm_id, data: data}, 34 | metadata: metadata, 35 | // TODO: need to _encode_ any buffers into base64 (JSON.stringify just drops them) 36 | buffers: buffers || [], 37 | // this doesn't seem relevant to the widget? 38 | header: {} 39 | }; 40 | 41 | const msg_txt = JSON.stringify(msg); 42 | 43 | // Since ipyleaflet can send mousemove events very quickly when hovering over the map, 44 | // we throttle them to ensure that the server doesn't get overwhelmed. Said events 45 | // generate a payload that looks like this: 46 | // {"method": "custom", "content": {"event": "interaction", "type": "mousemove", "coordinates": [-17.76259815404015, 12.096729340756617]}} 47 | // 48 | // TODO: This is definitely not ideal. It would be better to have a way to specify/ 49 | // customize throttle rates instead of having such a targetted fix for ipyleaflet. 50 | const is_mousemove = 51 | data.method === "custom" && 52 | data.content.event === "interaction" && 53 | data.content.type === "mousemove"; 54 | 55 | if (is_mousemove) { 56 | this.throttler.throttle(() => { 57 | Shiny.setInputValue("shinywidgets_comm_send", msg_txt, {priority: "event"}); 58 | }); 59 | } else { 60 | this.throttler.flush(); 61 | Shiny.setInputValue("shinywidgets_comm_send", msg_txt, {priority: "event"}); 62 | } 63 | 64 | // When client-side changes happen to the WidgetModel, this send method 65 | // won't get called for _every_ change (just the first one). The 66 | // expectation is that this method will eventually end up calling itself 67 | // (via callbacks) when the server is ready (i.e., idle) to receive more 68 | // updates. To make sense of this, see 69 | // https://github.com/jupyter-widgets/ipywidgets/blob/88cec8b/packages/base/src/widget.ts#L550-L557 70 | if (callbacks && callbacks.iopub && callbacks.iopub.status) { 71 | setTimeout(() => { 72 | // TODO-future: it doesn't seem quite right to report that shiny is always idle. 73 | // Maybe listen to the shiny-busy flag? 74 | // const state = document.querySelector("html").classList.contains("shiny-busy") ? "busy" : "idle"; 75 | const msg = {content: {execution_state: "idle"}}; 76 | callbacks.iopub.status(msg); 77 | }, 0); 78 | } 79 | 80 | return this.comm_id; 81 | } 82 | 83 | open( 84 | data: any, 85 | callbacks: any, 86 | metadata?: any, 87 | buffers?: ArrayBuffer[] | ArrayBufferView[] 88 | ): string { 89 | // I don't think we need to do anything here? 90 | return this.comm_id; 91 | } 92 | 93 | close( 94 | data?: any, 95 | callbacks?: any, 96 | metadata?: any, 97 | buffers?: ArrayBuffer[] | ArrayBufferView[] 98 | ): string { 99 | // I don't think we need to do anything here? 100 | return this.comm_id; 101 | } 102 | 103 | on_msg(callback: (x: any) => void): void { 104 | this._msg_callback = callback.bind(this); 105 | } 106 | 107 | on_close(callback: (x: any) => void): void { 108 | this._close_callback = callback.bind(this); 109 | } 110 | 111 | handle_msg(msg: any) { 112 | if (this._msg_callback) this._msg_callback(msg); 113 | } 114 | 115 | handle_close(msg: any) { 116 | if (this._close_callback) this._close_callback(msg); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /js/src/output.ts: -------------------------------------------------------------------------------- 1 | import { HTMLManager, requireLoader } from '@jupyter-widgets/html-manager'; 2 | import { ShinyComm } from './comm'; 3 | import { jsonParse } from './utils'; 4 | import type { ErrorsMessageValue } from 'rstudio-shiny/srcts/types/src/shiny/shinyapp'; 5 | 6 | 7 | /****************************************************************************** 8 | * Define a custom HTMLManager for use with Shiny 9 | ******************************************************************************/ 10 | 11 | class OutputManager extends HTMLManager { 12 | // In a soon-to-be-released version of @jupyter-widgets/html-manager, 13 | // display_view()'s first "dummy" argument will be removed... this shim simply 14 | // makes it so that our manager can work with either version 15 | // https://github.com/jupyter-widgets/ipywidgets/commit/159bbe4#diff-45c126b24c3c43d2cee5313364805c025e911c4721d45ff8a68356a215bfb6c8R42-R43 16 | async display_view(view: any, options: { el: HTMLElement; }): Promise { 17 | const n_args = super.display_view.length 18 | if (n_args === 3) { 19 | return super.display_view({}, view, options) 20 | } else { 21 | // @ts-ignore 22 | return super.display_view(view, options) 23 | } 24 | } 25 | } 26 | 27 | // Define our own custom module loader for Shiny 28 | const shinyRequireLoader = async function(moduleName: string, moduleVersion: string): Promise { 29 | 30 | // shiny provides a shim of require.js which allows