diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index f362e6edb95..16135a21bf2 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -8,6 +8,19 @@ on:
branches:
- '*'
workflow_dispatch:
+ inputs:
+ target:
+ description: "How much of the test suite to run"
+ type: choice
+ default: default
+ options:
+ - default
+ - full
+ - downstream
+ cache:
+ description: "Use cache"
+ type: boolean
+ default: true
schedule:
- cron: '0 19 * * SUN'
@@ -21,16 +34,82 @@ jobs:
runs-on: 'ubuntu-latest'
steps:
- uses: holoviz-dev/holoviz_tasks/pre-commit@v0.1a19
+ setup:
+ name: Setup workflow
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ outputs:
+ code_change: ${{ steps.filter.outputs.code }}
+ doc_change: ${{ steps.filter.outputs.doc }}
+ matrix: ${{ env.MATRIX }}
+ steps:
+ - uses: actions/checkout@v3
+ if: github.event_name != 'pull_request'
+ - name: Check for code changes
+ uses: dorny/paths-filter@v2.11.1
+ id: filter
+ with:
+ filters: |
+ code:
+ - 'panel/**'
+ - 'examples/**'
+ - 'setup.py'
+ - 'pyproject.toml'
+ - 'tox.ini'
+ - '.github/workflows/test.yaml'
+ doc:
+ - 'doc/getting_started/**'
+ - 'doc/how_to/**'
+ - 'scripts/**'
+ - name: Set matrix option
+ run: |
+ if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then
+ OPTION=${{ github.event.inputs.target }}
+ elif [[ '${{ github.event_name }}' == 'schedule' ]]; then
+ OPTION="full"
+ elif [[ '${{ github.event_name }}' == 'push' && '${{ github.ref_type }}' == 'tag' ]]; then
+ OPTION="full"
+ else
+ OPTION="default"
+ fi
+ echo "MATRIX_OPTION=$OPTION" >> $GITHUB_ENV
+ - name: Set test matrix with 'default' option
+ if: env.MATRIX_OPTION == 'default'
+ run: |
+ MATRIX=$(jq -nsc '{
+ "os": ["ubuntu-latest", "macos-latest", "windows-latest"],
+ "python-version": ["3.9", "3.11"],
+ "include": [
+ {"os": "ubuntu-latest", "python-version": "3.10"}
+ ]
+ }')
+ echo "MATRIX=$MATRIX" >> $GITHUB_ENV
+ - name: Set test matrix with 'full' option
+ if: env.MATRIX_OPTION == 'full'
+ run: |
+ MATRIX=$(jq -nsc '{
+ "os": ["ubuntu-latest", "macos-latest", "windows-latest"],
+ "python-version": ["3.9", "3.10", "3.11"]
+ }')
+ echo "MATRIX=$MATRIX" >> $GITHUB_ENV
+ - name: Set test matrix with 'downstream' option
+ if: env.MATRIX_OPTION == 'downstream'
+ run: |
+ MATRIX=$(jq -nsc '{
+ "os": ["ubuntu-latest"],
+ "python-version": ["3.11"]
+ }')
+ echo "MATRIX=$MATRIX" >> $GITHUB_ENV
+
unit_test_suite:
name: Unit tests on ${{ matrix.os }} with Python ${{ matrix.python-version }}
- needs: [pre_commit]
+ needs: [pre_commit, setup]
runs-on: ${{ matrix.os }}
+ if: needs.setup.outputs.code_change == 'true'
strategy:
fail-fast: false
- matrix:
- os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
- # Run on the full set on schedule, workflow_dispatch and push&tags events, otherwise on a subset.
- python-version: ${{ ( github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || ( github.event_name == 'push' && github.ref_type == 'tag' ) ) && fromJSON('["3.9", "3.10", "3.11"]') || fromJSON('["3.9", "3.11"]') }}
+ matrix: ${{ fromJson(needs.setup.outputs.matrix) }}
timeout-minutes: 90
defaults:
run:
@@ -56,7 +135,7 @@ jobs:
conda-update: true
nodejs: true
envs: -o examples -o recommended -o tests -o build
- cache: true
+ cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }}
opengl: true
id: install
- name: doit develop_install
@@ -98,8 +177,9 @@ jobs:
fail_ci_if_error: false # optional (default = false)
ui_test_suite:
name: UI tests on ${{ matrix.os }} with Python 3.9
- needs: [pre_commit]
+ needs: [pre_commit, setup]
runs-on: ${{ matrix.os }}
+ if: needs.setup.outputs.code_change == 'true' || needs.setup.outputs.doc_change == 'true'
strategy:
fail-fast: false
matrix:
@@ -144,7 +224,7 @@ jobs:
python-version: 3.9
channels: pyviz/label/dev,bokeh,conda-forge,nodefaults
envs: "-o recommended -o tests -o build"
- cache: true
+ cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }}
nodejs: true
playwright: true
id: install
@@ -199,8 +279,9 @@ jobs:
fail_ci_if_error: false # optional (default = false)
core_test_suite:
name: Core tests on Python ${{ matrix.python-version }}, ${{ matrix.os }}
- needs: [pre_commit]
+ needs: [pre_commit, setup]
runs-on: ${{ matrix.os }}
+ if: needs.setup.outputs.code_change == 'true'
strategy:
fail-fast: false
matrix:
@@ -223,7 +304,7 @@ jobs:
# # channel-priority: strict
# channels: pyviz/label/dev,conda-forge,nodefaults
# envs: "-o tests_core -o tests_ci"
- # cache: true
+ # cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }}
# conda-update: true
# id: install
- uses: actions/checkout@v3
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5707fe41993..3a6e21dd319 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,17 +17,17 @@ repos:
exclude: \.min\.js$
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.6
+ rev: v0.1.9
hooks:
- id: ruff
files: panel/
- repo: https://github.com/pycqa/isort
- rev: 5.12.0
+ rev: 5.13.2
hooks:
- id: isort
name: isort (python)
- repo: https://github.com/hoxbro/clean_notebook
- rev: v0.1.13
+ rev: v0.1.14
hooks:
- id: clean-notebook
args: [-i, tags]
@@ -39,7 +39,7 @@ repos:
additional_dependencies:
- tomli
- repo: https://github.com/pre-commit/mirrors-prettier
- rev: v3.0.3
+ rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [css]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b3fc151095..865b7710308 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,58 @@
# Releases
+## Version 1.3.7
+
+Date: 2024-01-19
+
+This patch release focuses on a number of fixes and minor enhancements for the chat components and various other smaller improvements and fixes including docs improvements. In particular we want to highlight the new Ploomber deployment guide contributed by @neelash23. Next we want to welcome @jz314, @fayssalelmofatiche and @neelasha23 as new contributors and welcome back @SultanOrazbayev as a returning contributor. Lastly we want to thank the core contributor team, including @MarcSkovMadsen, @ahuang11, @maximlt, @Hoxbro and @philippjfr for their continued efforts maintaining Panel.
+
+### Enhancements
+
+- Add `filter_by` to `ChatMessage.serialize` ([#6090](https://github.com/holoviz/panel/pull/6090))
+- Support using an SVG for `ToggleIcon` ([#6127](https://github.com/holoviz/panel/pull/6127))
+- Add resizable param to `TextAreaInput` ([#6126](https://github.com/holoviz/panel/pull/6126))
+- Improve date and datetime picker functionality ([#6152](https://github.com/holoviz/panel/pull/6152))
+- Add activity indicator to `ChatMessage` ([#6153](https://github.com/holoviz/panel/pull/6153))
+- Lazily import bleach HTML sanitizer ([#6179](https://github.com/holoviz/panel/pull/6179))
+
+### Bug fixes
+
+- Fix alignment issues in chat components ([#6104](https://github.com/holoviz/panel/pull/6104), [#6135](https://github.com/holoviz/panel/pull/6135))
+- Fix generator placeholder and optimize updates in Chat components ([#6105](https://github.com/holoviz/panel/pull/6105))
+- Fix issue with callback future handling on Chat components ([#6120](https://github.com/holoviz/panel/pull/6120))
+- Fix bug in Chat interfaces related to `pn.state.browser_info` ([#6122](https://github.com/holoviz/panel/pull/6122))
+- Allow instantiating empty `Matplotlib` pane ([#6128](https://github.com/holoviz/panel/pull/6128))
+- Ensure icon displays inline with text on `FileDownload` ([#6133](https://github.com/holoviz/panel/pull/6133))
+- Fix styling of links in `Tabulator` fast theme ([#6146](https://github.com/holoviz/panel/pull/6146))
+- Fix passing of `card_params` on `ChatFeed` ([#6154](https://github.com/holoviz/panel/pull/6154))
+- Handle `Tabulator.title_formatter` if is type `dict` ([#6166](https://github.com/holoviz/panel/pull/6166))
+- Fix `per_session` caching ([#6169](https://github.com/holoviz/panel/pull/6169))
+- Correctly reshape nd-arrays in `Plotly` pane ([#6174](https://github.com/holoviz/panel/pull/6174))
+- Handle NaT values on `Perspective` pane ([#6176](https://github.com/holoviz/panel/pull/6176))
+- Do not rerender output if `ReplacementPane` object identity is unchanged ([#6183](https://github.com/holoviz/panel/pull/6183))
+- Tabulator: fix valuesLookup set up for older list-like editors ([#6192](https://github.com/holoviz/panel/pull/6192))
+- Fix pyodide loading message styling issues ([#6194](https://github.com/holoviz/panel/pull/6194))
+- More complete patch for the `TextEditor` to support being rendered in the Shadow DOM ([#6222](https://github.com/holoviz/panel/pull/6222))
+- Add guard to `Tabulator` ensuring that it does not error when it is not rendered ([#6223](https://github.com/holoviz/panel/pull/6223))
+- Fix race conditions when instantiating Comm in Jupyter causing notifications to break ([#6229](https://github.com/holoviz/panel/pull/6229))
+
+### Compatibility & Security
+
+- Upgrade Plotly.js to 2.25.3 to address CVE-2023-46308 ([#6230](https://github.com/holoviz/panel/pull/6230))
+
+### Documentation
+
+- Add `Design` and `Theme` explanation documentation ([#4741](https://github.com/holoviz/panel/pull/4741))
+- Fix pyodide execution in documentation
+- Fix wrong and broken link ([#5988](https://github.com/holoviz/panel/pull/5988), [#6132](https://github.com/holoviz/panel/pull/6132))
+- Use GoatCounter for website analytics ([#6117](https://github.com/holoviz/panel/pull/6117))
+- Add Dask How to guide ([#4234](https://github.com/holoviz/panel/pull/4234))
+- Fix `Material` template notebook .show() call ([#6137](https://github.com/holoviz/panel/pull/6137))
+- Add missing item in docstring ([#6167](https://github.com/holoviz/panel/pull/6167))
+- Ploomber Cloud deployment documentation ([#6182](https://github.com/holoviz/panel/pull/6182))
+- Correct duplicate wording ([#6188](https://github.com/holoviz/panel/pull/6188))
+- Update JupyterLite Altair example to latest API ([#6226](https://github.com/holoviz/panel/pull/6226))
+
## Version 1.3.6
Date: 2023-12-20
diff --git a/doc/_static/images/asyncify.png b/doc/_static/images/asyncify.png
new file mode 100644
index 00000000000..527976db87f
Binary files /dev/null and b/doc/_static/images/asyncify.png differ
diff --git a/doc/_static/images/dask-dashboard-empty.png b/doc/_static/images/dask-dashboard-empty.png
new file mode 100644
index 00000000000..91473648b6f
Binary files /dev/null and b/doc/_static/images/dask-dashboard-empty.png differ
diff --git a/doc/_static/images/dask_fibonacci_queue.png b/doc/_static/images/dask_fibonacci_queue.png
new file mode 100644
index 00000000000..6ecbca2760d
Binary files /dev/null and b/doc/_static/images/dask_fibonacci_queue.png differ
diff --git a/doc/_static/images/ploomber_deployment.png b/doc/_static/images/ploomber_deployment.png
new file mode 100644
index 00000000000..922c46c7808
Binary files /dev/null and b/doc/_static/images/ploomber_deployment.png differ
diff --git a/doc/_static/logos/dask-logo.svg b/doc/_static/logos/dask-logo.svg
new file mode 100644
index 00000000000..3ac766541b8
--- /dev/null
+++ b/doc/_static/logos/dask-logo.svg
@@ -0,0 +1,18 @@
+
diff --git a/doc/_static/logos/ploomber.png b/doc/_static/logos/ploomber.png
new file mode 100644
index 00000000000..9a18e0903f4
Binary files /dev/null and b/doc/_static/logos/ploomber.png differ
diff --git a/doc/about/releases.md b/doc/about/releases.md
index 6d1e9d54bdf..d6af9fc7e02 100644
--- a/doc/about/releases.md
+++ b/doc/about/releases.md
@@ -2,6 +2,59 @@
See [the HoloViz blog](https://blog.holoviz.org/#category=panel) for a visual summary of the major features added in each release.
+## Version 1.3.7
+
+Date: 2024-01-19
+
+This patch release focuses on a number of fixes and minor enhancements for the chat components and various other smaller improvements and fixes including docs improvements. In particular we want to highlight the new Ploomber deployment guide contributed by @neelash23. Next we want to welcome @jz314, @fayssalelmofatiche and @neelasha23 as new contributors and welcome back @SultanOrazbayev as a returning contributor. Lastly we want to thank the core contributor team, including @MarcSkovMadsen, @ahuang11, @maximlt, @Hoxbro and @philippjfr for their continued efforts maintaining Panel.
+
+### Enhancements
+
+- Add `filter_by` to `ChatMessage.serialize` ([#6090](https://github.com/holoviz/panel/pull/6090))
+- Support using an SVG for `ToggleIcon` ([#6127](https://github.com/holoviz/panel/pull/6127))
+- Add resizable param to `TextAreaInput` ([#6126](https://github.com/holoviz/panel/pull/6126))
+- Improve date and datetime picker functionality ([#6152](https://github.com/holoviz/panel/pull/6152))
+- Add activity indicator to `ChatMessage` ([#6153](https://github.com/holoviz/panel/pull/6153))
+- Lazily import bleach HTML sanitizer ([#6179](https://github.com/holoviz/panel/pull/6179))
+
+### Bug fixes
+
+- Fix alignment issues in chat components ([#6104](https://github.com/holoviz/panel/pull/6104), [#6135](https://github.com/holoviz/panel/pull/6135))
+- Fix generator placeholder and optimize updates in Chat components ([#6105](https://github.com/holoviz/panel/pull/6105))
+- Fix issue with callback future handling on Chat components ([#6120](https://github.com/holoviz/panel/pull/6120))
+- Fix bug in Chat interfaces related to `pn.state.browser_info` ([#6122](https://github.com/holoviz/panel/pull/6122))
+- Allow instantiating empty `Matplotlib` pane ([#6128](https://github.com/holoviz/panel/pull/6128))
+- Ensure icon displays inline with text on `FileDownload` ([#6133](https://github.com/holoviz/panel/pull/6133))
+- Fix styling of links in `Tabulator` fast theme ([#6146](https://github.com/holoviz/panel/pull/6146))
+- Fix passing of `card_params` on `ChatFeed` ([#6154](https://github.com/holoviz/panel/pull/6154))
+- Handle `Tabulator.title_formatter` if is type `dict` ([#6166](https://github.com/holoviz/panel/pull/6166))
+- Fix `per_session` caching ([#6169](https://github.com/holoviz/panel/pull/6169))
+- Correctly reshape nd-arrays in `Plotly` pane ([#6174](https://github.com/holoviz/panel/pull/6174))
+- Handle NaT values on `Perspective` pane ([#6176](https://github.com/holoviz/panel/pull/6176))
+- Do not rerender output if `ReplacementPane` object identity is unchanged ([#6183](https://github.com/holoviz/panel/pull/6183))
+- Tabulator: fix valuesLookup set up for older list-like editors ([#6192](https://github.com/holoviz/panel/pull/6192))
+- Fix pyodide loading message styling issues ([#6194](https://github.com/holoviz/panel/pull/6194))
+- More complete patch for the `TextEditor` to support being rendered in the Shadow DOM ([#6222](https://github.com/holoviz/panel/pull/6222))
+- Add guard to `Tabulator` ensuring that it does not error when it is not rendered ([#6223](https://github.com/holoviz/panel/pull/6223))
+- Fix race conditions when instantiating Comm in Jupyter causing notifications to break ([#6229](https://github.com/holoviz/panel/pull/6229))
+
+### Compatibility & Security
+
+- Upgrade Plotly.js to 2.25.3 to address CVE-2023-46308 ([#6230](https://github.com/holoviz/panel/pull/6230))
+
+### Documentation
+
+- Add `Design` and `Theme` explanation documentation ([#4741](https://github.com/holoviz/panel/pull/4741))
+- Fix pyodide execution in documentation
+- Fix wrong and broken link ([#5988](https://github.com/holoviz/panel/pull/5988), [#6132](https://github.com/holoviz/panel/pull/6132))
+- Use GoatCounter for website analytics ([#6117](https://github.com/holoviz/panel/pull/6117))
+- Add Dask How to guide ([#4234](https://github.com/holoviz/panel/pull/4234))
+- Fix `Material` template notebook .show() call ([#6137](https://github.com/holoviz/panel/pull/6137))
+- Add missing item in docstring ([#6167](https://github.com/holoviz/panel/pull/6167))
+- Ploomber Cloud deployment documentation ([#6182](https://github.com/holoviz/panel/pull/6182))
+- Correct duplicate wording ([#6188](https://github.com/holoviz/panel/pull/6188))
+- Update JupyterLite Altair example to latest API ([#6226](https://github.com/holoviz/panel/pull/6226))
+
## Version 1.3.6
Date: 2023-12-20
diff --git a/doc/conf.py b/doc/conf.py
index a4e2a5d8f64..943fb2e00b4 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -81,7 +81,8 @@
'sphinx.ext.napoleon',
'nbsite.gallery',
'sphinx_copybutton',
- 'nbsite.pyodide'
+ 'nbsite.pyodide',
+ 'nbsite.analytics',
]
napoleon_numpy_docstring = True
@@ -92,6 +93,10 @@
jlite_url = 'https://pyviz-dev.github.io/panelite-dev' if is_dev else 'https://panelite.holoviz.org'
pyodide_url = 'https://pyviz-dev.github.io/panel/pyodide' if is_dev else 'https://panel.holoviz.org/pyodide'
+nbsite_analytics = {
+ 'goatcounter_holoviz': True,
+}
+
nbsite_gallery_conf = {
'github_org': 'holoviz',
'github_project': 'panel',
diff --git a/doc/explanation/index.md b/doc/explanation/index.md
index da6f70ff750..8c3b98d07b9 100644
--- a/doc/explanation/index.md
+++ b/doc/explanation/index.md
@@ -58,14 +58,22 @@ Deepen your understanding about how Panel communicates between Python and Javasc
::::{grid} 1 2 2 3
:gutter: 1 1 1 2
+:::{grid-item-card} {octicon}`paintbrush;2.5em;sd-mr-1 sd-animate-grow50` Designs & Themes
+:link: styling/design
+:link-type: doc
+
+Understand how the Panel `Design` and `Theme` components work internally.
+:::
+
:::{grid-item-card} {octicon}`repo-template;2.5em;sd-mr-1 sd-animate-grow50` Templates
-:link: templates/templates_overview
+:link: styling/templates_overview
:link-type: doc
-Deepen your understanding about Template styling in Panel.
+Discover Panel templates and how to use them.
:::
::::
+
## Dependencies
::::{grid} 1 2 2 3
diff --git a/doc/explanation/styling.md b/doc/explanation/styling.md
deleted file mode 100644
index f89d51aba1e..00000000000
--- a/doc/explanation/styling.md
+++ /dev/null
@@ -1,14 +0,0 @@
-.. raw:: html
-
-
-
-
-# Styling
-
-```{toctree}
-:titlesonly:
-:hidden:
-:maxdepth: 1
-
-Templates
-```
diff --git a/doc/explanation/styling/design.md b/doc/explanation/styling/design.md
new file mode 100644
index 00000000000..af32ffa31be
--- /dev/null
+++ b/doc/explanation/styling/design.md
@@ -0,0 +1,110 @@
+# Designs and Theming
+
+:::{versionadded} 1.0.0
+Designs and theming allow controlling the look and feel of individual components or your entire application. Before version 1.0 the concept of a design and a template were combined but since it is now possible to apply a `Design` separately from a template.
+:::
+
+## `Design` class
+
+The `Design` class has the ability to apply styling and theming to components via three mechanisms:
+
+1. Loading external JS and CSS `resources`.
+2. Applying `modifiers` that directly override (or combine with) component parameter settings (including CSS stylesheets).
+
+Additionally a `Design` class always has an associated `Theme` which determines the color palette of the design. This also has the ability to:
+
+1. Apply a `base_css` (usually containing CSS variable definitions and inherited from the base class).
+2. Apply a `bokeh_theme` to override Bokeh component styling.
+3. Apply a `css` with specific CSS overrides for that theme.
+4. Apply its own set of `modifiers`.
+
+### Example: Bootstrap
+
+Let us work through the definition of the `Bootstrap` design step-by-step.
+
+#### Resources
+
+First of all, the `Bootstrap` design defines CSS and JS `resources` which load the bootstrap5 library:
+
+```python
+ _resources = {
+ 'css': {
+ 'bootstrap': CSS_URLS['bootstrap5']
+ },
+ 'js': {
+ 'bootstrap': JS_URLS['bootstrap5']
+ }
+ }
+```
+
+#### Modifiers
+
+Next we get to the `modifiers` which is where the meat of the styling definition lives. Modifiers are applied per class and override the default parameter settings of a component. We can apply modifiers to a base class such as `Viewable`, which will affect all components or we can override a specific class, e.g. `Tabulator`:
+
+```python
+ modifiers = {
+ Accordion: {
+ 'active_header_background': 'var(--bs-surface-bg)'
+ }
+ ...
+ Tabulator: {
+ 'theme': 'bootstrap4'
+ },
+ Viewable: {
+ 'stylesheets': [Inherit, f'{CDN_DIST}bundled/theme/bootstrap.css']
+ }
+ }
+```
+
+Here we define specific parameters to override or combine with, e.g. we set the `active_header_background` of an `Accordion` to inherit from the `--bs-surface-bg` CSS variable. In this way we can control settings that are not controlled by CSS. However, most crucially we also declare that all `Viewable` components should inherit a bundled stylesheet: `bundled/theme/bootstrap.css`.
+
+The `bootstrap.css` file includes styling overrides for many different components and will be loaded by all `Viewable` components.
+
+#### CSS stylesheets
+
+All design stylesheets open with CSS variable definitions which map the global color definitions to specific variables used by a particular design system:
+
+```css
+:host, :root {
+ --bs-primary-color: var(--jp-ui-font-color0, var(--panel-on-primary-color));
+ --bs-primary-bg: var(--jp-brand-color0, var(--panel-primary-color));
+}
+```
+
+The `:host` and `:root` selectors ensure that these variable definition apply inside the shadow DOM, i.e. that they apply to the shadow `host`, and globally to the document `root` (i.e. usually the `` element).
+
+Note also how we map the `--bs-primary-color` color to two other variables, first the `--jp-ui-font-color0`, which allows us to automatically inherit styles in Jupyter based environments and secondly the `--panel-on-primary-color` which is the primary definition of Panel color themes and also the entrypoint for users to override the color definitions.
+
+Once declared these CSS variables are then used to set the style of specific components:
+
+```css
+.bk-menu {
+ background-color: var(--bs-form-control-bg);
+ color: var(--bs-form-control-color);
+}
+```
+
+In this way global style definitions and variables can flow down all the way to individual components.
+
+#### Themes
+
+Lastly we get to the themes, each `Design` component declares the set of supported themes by name:
+
+```python
+ _themes = {
+ 'dark': BootstrapDarkTheme,
+ 'default': BootstrapDefaultTheme,
+ }
+```
+
+Currently only `default` (light) and `dark` themes are supported. The implementation of these themes subclasses from the `DefaultTheme` and `DarkTheme` base classes and applies specific overrides.
+
+## Developing an internal theme
+
+If you want to help contribute improvements to a `Design` or `Theme` simply edit the CSS definitions in `panel/theme/css` and periodically re-bundle the resources with:
+
+```bash
+panel bundle --themes --only-local
+```
+
+On UNIX you can simply use `watch -n 5 panel bundle --themes --only-local` to re-bundle these resources every few seconds.
diff --git a/doc/explanation/styling/index.md b/doc/explanation/styling/index.md
new file mode 100644
index 00000000000..dcb886bc077
--- /dev/null
+++ b/doc/explanation/styling/index.md
@@ -0,0 +1,15 @@
+.. raw:: html
+
+
+
+
+# Styling
+
+```{toctree}
+:titlesonly:
+:hidden:
+:maxdepth: 1
+
+Design & Theme
+Templates
+```
diff --git a/doc/explanation/templates/templates_overview.md b/doc/explanation/styling/templates_overview.md
similarity index 100%
rename from doc/explanation/templates/templates_overview.md
rename to doc/explanation/styling/templates_overview.md
diff --git a/doc/getting_started/core_concepts.md b/doc/getting_started/core_concepts.md
index e6f7140d671..dfd1431ebd5 100644
--- a/doc/getting_started/core_concepts.md
+++ b/doc/getting_started/core_concepts.md
@@ -93,7 +93,7 @@ Row
```
:::
-Sometimes an object has multiple possible representations to pick from. In these cases you can explicitly construct construct the desired `Pane` type, e.g. here are a few representations of a `DataFrame`:
+Sometimes an object has multiple possible representations to pick from. In these cases you can explicitly construct the desired `Pane` type, e.g. here are a few representations of a `DataFrame`:
::::{tab-set}
diff --git a/doc/how_to/callbacks/async.md b/doc/how_to/callbacks/async.md
index 92156ed2667..d43eb3bcfe3 100644
--- a/doc/how_to/callbacks/async.md
+++ b/doc/how_to/callbacks/async.md
@@ -1,13 +1,22 @@
# Use Asynchronous Callbacks
-This guide addresses how to leverage asynchronous callbacks to run I/O bound tasks in parallel.
+This guide addresses how to leverage asynchronous callbacks to run I/O bound tasks in parallel. This technique is also beneficial for CPU bound tasks that release the GIL.
-```{admonition} Prerequisites
-1. Python has natively supported asynchronous functions since version 3.5, for a quick overview of some of the concepts involved see [the Python documentation](https://docs.python.org/3/library/asyncio-task.html).
+You can use `async` function with event handlers like `on_click` as well as the reactive apis `.bind`, `.depends` and `.watch`.
+
+You can also schedule asynchronous periodic callbacks with `pn.state.add_periodic_callback` as well as run `async` functions directly with `pn.state.execute`.
+
+```{admonition} Asyncio
+For a quick overview of the most important `asyncio` concepts see [the Python documentation](https://docs.python.org/3/library/asyncio-task.html).
+```
+
+```{admonition} Bokeh Models
+It is important to note that asynchronous callbacks operate without locking the underlying Bokeh Document, which means Bokeh models cannot be safely modified by default. Usually this is not an issue because modifying Panel components appropriately schedules updates to underlying Bokeh models, however in cases where we want to modify a Bokeh model directly, e.g. when embedding and updating a Bokeh plot in a Panel application we explicitly have to decorate the asynchronous callback with `pn.io.with_lock` (see example below).
```
+
---
-## `.param.watch`
+## `on_click`
One of the major benefits of leveraging async functions is that it is simple to write callbacks which will perform some longer running IO tasks in the background. Below we simulate this by creating a `Button` which will update some text when it starts and finishes running a long-running background task (here simulated using `asyncio.sleep`. If you are running this in the notebook you will note that you can start multiple tasks and it will update the text immediately but continue in the background:
@@ -30,36 +39,7 @@ button.on_click(run_async)
pn.Row(button, text)
```
-Note that `on_click` is simple one way of registering an asynchronous callback, but the more flexible `.param.watch` is also supported. Scheduling asynchronous periodic callbacks can be done with `pn.state.add_periodic_callback`.
-
-It is important to note that asynchronous callbacks operate without locking the underlying Bokeh Document, which means Bokeh models cannot be safely modified by default. Usually this is not an issue because modifying Panel components appropriately schedules updates to underlying Bokeh models, however in cases where we want to modify a Bokeh model directly, e.g. when embedding and updating a Bokeh plot in a Panel application we explicitly have to decorate the asynchronous callback with `pn.io.with_lock`.
-
-```{pyodide}
-import numpy as np
-from bokeh.plotting import figure
-from bokeh.models import ColumnDataSource
-
-button = pn.widgets.Button(name='Click me!')
-
-p = figure(width=500, height=300)
-cds = ColumnDataSource(data={'x': [0], 'y': [0]})
-p.line(x='x', y='y', source=cds)
-pane = pn.pane.Bokeh(p)
-
-@pn.io.with_lock
-async def stream(event):
- await asyncio.sleep(1)
- x, y = cds.data['x'][-1], cds.data['y'][-1]
- cds.stream({'x': list(range(x+1, x+6)), 'y': y+np.random.randn(5).cumsum()})
- pane.param.trigger('object')
-
-# Equivalent to `.on_click` but shown
-button.param.watch(stream, 'clicks')
-
-pn.Row(button, pane)
-```
-
-## `pn.bind`
+## `.bind`
```{pyodide}
widget = pn.widgets.IntSlider(start=0, end=10)
@@ -80,7 +60,9 @@ pn.Column(widget, pn.bind(get_img, widget))
In this example Panel will invoke the function and update the output when the function returns while leaving the process unblocked for the duration of the `aiohttp` request.
-The equivalent can be written using `.param.watch` as:
+## `.watch`
+
+The app from the section above can be written using `.param.watch` as:
```{pyodide}
widget = pn.widgets.IntSlider(start=0, end=10)
@@ -107,6 +89,33 @@ pn.Column(widget, image)
In this example Param will await the asynchronous function and the image will be updated when the request completes.
+## Bokeh models with `pn.io.with_lock`
+
+```{pyodide}
+import numpy as np
+from bokeh.plotting import figure
+from bokeh.models import ColumnDataSource
+
+button = pn.widgets.Button(name='Click me!')
+
+p = figure(width=500, height=300)
+cds = ColumnDataSource(data={'x': [0], 'y': [0]})
+p.line(x='x', y='y', source=cds)
+pane = pn.pane.Bokeh(p)
+
+@pn.io.with_lock
+async def stream(event):
+ await asyncio.sleep(1)
+ x, y = cds.data['x'][-1], cds.data['y'][-1]
+ cds.stream({'x': list(range(x+1, x+6)), 'y': y+np.random.randn(5).cumsum()})
+ pane.param.trigger('object')
+
+# Equivalent to `.on_click` but shown
+button.param.watch(stream, 'clicks')
+
+pn.Row(button, pane)
+```
+
## Related Resources
- See the related [How-to > Link Parameters with Callbacks API](../links/index.md) guides, including [How to > Create Low-Level Python Links Using `.watch`](../links/watchers.md).
diff --git a/doc/how_to/concurrency/async.md b/doc/how_to/concurrency/async.md
deleted file mode 100644
index 7fb7494bab0..00000000000
--- a/doc/how_to/concurrency/async.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Use Asynchronous Processing
-
-When using Python you can use async callbacks wherever you would ordinarily use a regular synchronous function. For instance you can use `pn.bind` on an async function:
-
-```{pyodide}
-import panel as pn
-
-widget = pn.widgets.IntSlider(start=0, end=10)
-
-async def get_img(index):
- url = f"https://picsum.photos/800/300?image={index}"
- if pn.state._is_pyodide:
- from pyodide.http import pyfetch
- return pn.pane.JPG(await (await pyfetch(url)).bytes())
-
- import aiohttp
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as resp:
- return pn.pane.JPG(await resp.read())
-
-pn.Column(widget, pn.bind(get_img, widget))
-```
-
-In this example Panel will invoke the function and update the output when the function returns while leaving the process unblocked for the duration of the `aiohttp` request.
-
-Similarly you can attach asynchronous callbacks using `.param.watch`:
-
-```{pyodide}
-widget = pn.widgets.IntSlider(start=0, end=10)
-
-image = pn.pane.JPG()
-
-async def update_img(event):
- url = f"https://picsum.photos/800/300?image={event.new}"
- if pn.state._is_pyodide:
- from pyodide.http import pyfetch
- image.object = await (await pyfetch(url)).bytes()
- return
-
- import aiohttp
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as resp:
- image.object = await resp.read()
-
-widget.param.watch(update_img, 'value')
-widget.param.trigger('value')
-
-pn.Column(widget, image)
-```
-
-In this example Param will await the asynchronous function and the image will be updated when the request completes.
diff --git a/doc/how_to/concurrency/dask.md b/doc/how_to/concurrency/dask.md
new file mode 100644
index 00000000000..bd015acf009
--- /dev/null
+++ b/doc/how_to/concurrency/dask.md
@@ -0,0 +1,235 @@
+# Scaling with Dask
+
+This guide demonstrates how you can *offload tasks* to [Dask](https://www.dask.org/) to **scale your apps to bigger datasets, bigger calculations and more users**.
+
+
+
+Panel supports `async` and `await`. This means you can easily **offload large computations to your Dask cluster asynchronously and keep your app responsive while you `await` the results**. Please note that off loading the computations to the Dask cluster can add ~250msec of overhead and thus is not suitable for all kinds of use cases.
+
+## Installation
+
+Lets start by installing *Panel*, *hvPlot* and *Dask Distributed*.
+
+```bash
+pip install panel hvplot dask[distributed]
+```
+
+## Start the Cluster
+
+For development, testing and many use cases a [`LocalCluster`](https://docs.dask.org/en/stable/deploying-python.html#localcluster) is more than fine and will allow you to leverage all the CPUs on your machine. When you want to scale out to an entire cluster will you can switch to a non-local cluster. To avoid any issues when combining Panel and Dask we recommend starting the `LocalCluster`
+separately from the Dask `Client` and your Panel app.
+
+```python
+# cluster.py
+from dask.distributed import LocalCluster
+
+DASK_SCHEDULER_PORT = 64719
+DASK_SCHEDULER_ADDRESS = f"tcp://127.0.0.1:{DASK_SCHEDULER_PORT}"
+N_WORKERS = 4
+
+if __name__ == '__main__':
+ cluster = LocalCluster(scheduler_port=DASK_SCHEDULER_PORT, n_workers=N_WORKERS)
+ print(cluster.scheduler_address)
+ input()
+```
+
+and running
+
+```bash
+$ python cluster.py
+tcp://127.0.0.1:64719
+```
+
+You can now open the [Dask Dashboard](https://docs.dask.org/en/stable/dashboard.html) at [http://localhost:8787/status](http://localhost:8787/status).
+
+So far there is not a lot to see here:
+
+![Empty Dask Dashboard](../../_static/images/dask-dashboard-empty.png)
+
+The Dask `Client` will serialize any *tasks* and send them to the Dask `Cluster` for execution. This means that the `Client` and `Cluster` must able to import the same versions of all *tasks* and python package dependencies.
+
+## Dask Distributed
+
+## Fibonacci Task Queue
+
+In this section we will define a Panel app to *submit* and *monitor* Fibonacci tasks.
+
+Let's start by defining the *fibonacci* tasks in a `tasks.py` file:
+
+```python
+# tasks.py
+from datetime import datetime as dt
+
+import numpy as np
+
+
+def _fib(n):
+ if n < 2:
+ return n
+ else:
+ return _fib(n - 1) + _fib(n - 2)
+
+
+def fibonacci(n):
+ start = dt.now()
+ print(start, "start", n)
+ result = _fib(n)
+ end = dt.now()
+ print(end, "end", (end-start).seconds, n, result)
+ return result
+```
+
+Lets now define the full `app.py` file.
+
+```python
+# app.py
+from datetime import datetime as dt
+
+from dask.distributed import Client
+
+import panel as pn
+
+from cluster import DASK_SCHEDULER_ADDRESS
+from tasks import fibonacci
+
+QUEUE = []
+
+pn.extension("terminal", design="material", sizing_mode="stretch_width")
+
+@pn.cache # We use caching to share the client across all users and sessions
+async def get_client():
+ return await Client(
+ DASK_SCHEDULER_ADDRESS, asynchronous=True
+ )
+
+n_input = pn.widgets.IntInput(value=0, width=100, sizing_mode="fixed", name="n")
+submit_button = pn.widgets.Button(name="SUBMIT", button_type="primary", align="end")
+terminal_widget = pn.widgets.Terminal(
+ height=200,
+)
+
+queue = pn.rx(QUEUE)
+
+@pn.depends(submit_button, watch=True)
+async def _handle_click(_):
+ n = n_input.value
+ n_input.value += 1
+
+ start = dt.now()
+ QUEUE.append(n)
+ queue.rx.value = QUEUE
+
+ client = await get_client()
+ fib_n = await client.submit(fibonacci, n)
+
+ end = dt.now()
+
+ QUEUE.pop(QUEUE.index(n))
+ queue.rx.value = QUEUE
+
+ duration = (end - start).seconds
+ terminal_widget.write(f"fibonacci({n})={fib_n} in {duration}sec\n")
+
+
+pn.Column(
+ "# Fibonacci Tasks",
+ pn.Row(n_input, submit_button),
+ pn.rx("## Task queue: {}").format(queue),
+ "## Results",
+ terminal_widget,
+).servable()
+```
+
+You can now run `panel serve app.py` and the app will look like
+
+
+
+## Dask Dashboard Components
+
+It can be very useful to include some of the live [Dask endpoints](https://distributed.dask.org/en/stable/http_services.html) in your app. Its easy to do by embedding the specific urls in an *iframe*.
+
+In the `dashboard.py` file we define the `DaskViewer` component that can be used to explore the *individual dask plots*.
+
+```python
+# dashboard.py
+import os
+
+import param
+
+import panel as pn
+
+DASK_DASHBOARD_ADDRESS = os.getenv("DASK_DASHBOARD", "http://localhost:8787/status")
+
+VIEWS = {
+ "aggregate-time-per-action": "individual-aggregate-time-per-action",
+ "bandwidth-types": "individual-bandwidth-types",
+ "bandwidth-workers": "individual-bandwidth-workers",
+ "cluster-memory": "individual-cluster-memory",
+ "compute-time-per-key": "individual-compute-time-per-key",
+ "cpu": "individual-cpu",
+ "exceptions": "individual-exceptions",
+ "gpu-memory": "individual-gpu-memory",
+ "gpu-utilization": "individual-gpu-utilization",
+ "graph": "individual-graph",
+ "groups": "individual-groups",
+ "memory-by-key": "individual-memory-by-key",
+ "nprocessing": "individual-nprocessing",
+ "occupancy": "individual-occupancy",
+ "profile-server": "individual-profile-server",
+ "profile": "individual-profile",
+ "progress": "individual-progress",
+ "scheduler-system": "individual-scheduler-system",
+ "task-stream": "individual-task-stream",
+ "workers-cpu-timeseries": "individual-workers-cpu-timeseries",
+ "workers-disk-timeseries": "individual-workers-disk-timeseries",
+ "workers-disk": "individual-workers-disk",
+ "workers-memory-timeseries": "individual-workers-memory-timeseries",
+ "workers-memory": "individual-workers-memory",
+ "workers-network-timeseries": "individual-workers-network-timeseries",
+ "workers-network": "individual-workers-network",
+ "workers": "individual-workers",
+}
+
+VIEWER_PARAMETERS = ["url", "path"]
+
+def dask_dashboard_view(path="individual-cpu", url=DASK_DASHBOARD_ADDRESS):
+ url = url.replace("/status", "/") + path
+ return f""""""
+
+class DaskViewer(pn.viewable.Viewer):
+ url = param.String(DASK_DASHBOARD_ADDRESS, doc="The url of the Dask status dashboard")
+ path = param.Selector(default="individual-cpu", objects=VIEWS, doc="the endpoint", label="View")
+
+ def __init__(self, size=20, **params):
+ viewer_params = {k:v for k, v in params.items() if k in VIEWER_PARAMETERS}
+ layout_params = {k:v for k, v in params.items() if k not in VIEWER_PARAMETERS}
+
+ super().__init__(**viewer_params)
+
+ view = pn.bind(dask_dashboard_view, self.param.path, self.param.url)
+ self._iframe = pn.pane.HTML(view, sizing_mode="stretch_both")
+ self._select = pn.widgets.Select.from_param(self.param.path, size=size, width=300, sizing_mode="fixed", margin=(20,5,10,5))
+ self._link = pn.panel(f"""Dask Dashboard""", height=50, margin=(0,20))
+ self._panel = pn.Column(pn.Row(self._iframe, self._select, sizing_mode="stretch_both"), self._link, **layout_params)
+
+ def __panel__(self):
+ return self._panel
+
+if __name__.startswith("bokeh"):
+ pn.extension(sizing_mode="stretch_width")
+
+ DaskViewer(height=500, size=25).servable()
+```
+
+Try running `panel serve dashboard.py`. If your Dask cluster is working, you will see something like
+
+![Dask Viewer](https://assets.holoviz.org/panel/how_to/concurrency/dask-dashboard.gif)
+
+## Additional Resources
+
+- [Panel - Use Async Callbacks](../callbacks/async.md)
+- [Dask - Async/Await and Non-Blocking Execution Documentation](https://examples.dask.org/applications/async-await.html#Async/Await-and-Non-Blocking-Execution)
+- [Dask - Async Web Server](https://examples.dask.org/applications/async-web-server.html)
diff --git a/doc/how_to/concurrency/index.md b/doc/how_to/concurrency/index.md
index 03be0a31c68..7e965bb0ef6 100644
--- a/doc/how_to/concurrency/index.md
+++ b/doc/how_to/concurrency/index.md
@@ -54,10 +54,33 @@ Discover how to manually set up a Thread to process an event queue.
:::
:::{grid-item-card} {octicon}`arrow-switch;2.5em;sd-mr-1 sd-animate-grow50` Use Asynchronous Processing
-:link: async
+:link: ../callbacks/async
:link-type: doc
-Discover how to make use of asynchronous callbacks to handle I/O bound operations concurrently.
+Discover how to make use of asynchronous callbacks to handle I/O and cpu bound operations concurrently.
+:::
+
+:::{grid-item-card} {octicon}`paper-airplane;2.5em;sd-mr-1 sd-animate-grow50` Sync to Async
+:link: sync_to_async
+:link-type: doc
+
+Discover how to run your sync callbacks asynchronously to handle I/O and cpu bound operations concurrently.
+:::
+
+::::
+
+## Scaling via an external compute engine
+
+You can also scale your application by offloading your compute heavy tasks to an external compute engine like [Dask](https://www.dask.org/). Please note that this may add additional overhead of several 100ms to your tasks.
+
+::::{grid} 1 2 2 2
+:gutter: 1 1 1 2
+
+:::{grid-item-card} {octicon}`versions;2.5em;sd-mr-1 sd-animate-grow50` Dask
+:link: dask
+:link-type: doc
+
+Discover how-to configure and use Dask to scale your Panel application
:::
::::
@@ -70,5 +93,8 @@ Discover how to make use of asynchronous callbacks to handle I/O bound operation
load_balancing
processes
threading
-async
+manual_threading
+../callbacks/async
+sync_to_async
+dask
```
diff --git a/doc/how_to/concurrency/sync_to_async.md b/doc/how_to/concurrency/sync_to_async.md
new file mode 100644
index 00000000000..cd80c3da17e
--- /dev/null
+++ b/doc/how_to/concurrency/sync_to_async.md
@@ -0,0 +1,40 @@
+# Run synchronous functions asynchronously
+
+Running your bound, synchronous functions asynchronously can be an easy way to make your application responsive and scale to more users.
+
+## Asyncify
+
+This example will show how to make your app responsive by running a sync, cpu bound function asynchronously. We will be using [asyncer](https://asyncer.tiangolo.com) by Tiangolo. You can install the package via `pip install asyncer`.
+
+```python
+import numpy as np
+import pandas as pd
+
+from asyncer import asyncify
+import panel as pn
+
+widget = pn.widgets.IntSlider(value=5, start=0, end=10)
+
+def do_sync_work(it, n):
+ return sum(pd.DataFrame(np.random.rand(n,n)).sum().sum() for _ in range(it))
+
+async def create_result():
+ yield pn.indicators.LoadingSpinner(value=True, width=25, height=25)
+ result = await asyncify(do_sync_work)(it=5, n=10000)
+ yield f"Wow. That was slow.\n\nThe sum is **{result:.2f}**"
+
+pn.Column(widget.rx() + 1, create_result).servable()
+```
+
+
+
+Without [`asyncify`](https://asyncer.tiangolo.com/tutorial/asyncify/) the app would have been unresponsive for 5-10 seconds while loading.
+
+[`asyncify`](https://asyncer.tiangolo.com/tutorial/asyncify/) works well for IO bound functions as well as for CPU bound functions that releases the GIL.
+
+## Dask
+
+If you run many cpu bound functions you may consider offloading your functions asynchronously to an external compute engine like [Dask](https://www.dask.org/). See our [Dask how-to Guide](../performance/dask.md).
diff --git a/doc/how_to/deployment/index.md b/doc/how_to/deployment/index.md
index 536345e0ae9..702d71ced1b 100644
--- a/doc/how_to/deployment/index.md
+++ b/doc/how_to/deployment/index.md
@@ -42,6 +42,13 @@ For guides on running and configuring a Panel server see the [server how-to guid
![Hugging Face Logo](../../_static/logos/huggingface.png)
:::
+:::{grid-item-card} Ploomber Cloud
+:link: ploomber
+:link-type: doc
+
+![Ploomber Logo](../../_static/logos/ploomber.png)
+:::
+
::::
## Other Cloud Providers
@@ -58,4 +65,5 @@ binder
gcp
heroku
huggingface
+ploomber
```
diff --git a/doc/how_to/deployment/ploomber.md b/doc/how_to/deployment/ploomber.md
new file mode 100644
index 00000000000..61ec362aa4a
--- /dev/null
+++ b/doc/how_to/deployment/ploomber.md
@@ -0,0 +1,49 @@
+# Ploomber Cloud
+
+Ploomber Cloud offers a [free deployment](https://platform.ploomber.io) option for Panel apps. Once you create an account and log in, follow these steps for deploying using the web application:
+
+1. Click on the "NEW" button. You'll find the below page:
+
+
+
+2. In the "Framework" section, click on Panel.
+3. In the "Source code" section, click on "Upload your files".
+4. Upload your `.zip` file with the `app.py` and `requirements.txt` file.
+5. Click on "CREATE"
+6. Wait until deployment finishes. To see your app, click on the `VIEW APPLICATION` button.
+
+
+Full instructions for deploying Panel apps are available [here](https://docs.cloud.ploomber.io/en/latest/apps/panel.html).
+
+You can also deploy the panel app using the Ploomber command line interface by following the below steps:
+
+1. First, install the package:
+
+```bash
+pip install ploomber-cloud
+```
+
+2. Get an [API Key](https://docs.cloud.ploomber.io/en/latest/quickstart/apikey.html) and set the key:
+
+```bash
+ploomber-cloud key YOURKEY
+```
+
+3. `cd` into the panel app folder that you want to deploy.
+4. Initialize your project:
+
+```bash
+ploomber-cloud init
+```
+
+5. This will prompt you for the project type. You need to enter `panel`.
+6. Once your project is configured it will generate a `ploomber-cloud.json` with the project info.
+7. Deploy the project by running:
+
+```bash
+ploomber-cloud deploy
+```
+
+8. To view your application, login to the web application and click on the `VIEW APPLICATION` button.
+
+Full instructions for deploying apps using the CLI are available [here](https://docs.cloud.ploomber.io/en/latest/user-guide/cli.html).
diff --git a/doc/how_to/links/jslinks.md b/doc/how_to/links/jslinks.md
index 15d2035f725..9b1837afe38 100644
--- a/doc/how_to/links/jslinks.md
+++ b/doc/how_to/links/jslinks.md
@@ -81,4 +81,4 @@ pn.Row(url, button)
## Related Resources
- See the [Explanation > APIs](../../explanation/api/index.md) for context on this and other Panel APIs
-- The [How to > Link Plot Parameters in Javascript](./links) guide addresses how to link Bokeh and HoloViews plot parameters in Javascript.
+- The [How to > Link Plot Parameters in Javascript](./link_plots.md) guide addresses how to link Bokeh and HoloViews plot parameters in Javascript.
diff --git a/doc/how_to/streamlit_migration/chat.md b/doc/how_to/streamlit_migration/chat.md
index a6e14baa959..4190c407aab 100644
--- a/doc/how_to/streamlit_migration/chat.md
+++ b/doc/how_to/streamlit_migration/chat.md
@@ -4,15 +4,15 @@ Both Streamlit and Panel provides special components to help you build conversat
| Streamlit | Panel | Description |
| -------------------- | ------------------- | -------------------------------------- |
-| [`chat_message`](https://docs.streamlit.io/library/api-reference/chat/st.chat_message) | [`ChatMessage`](../../../examples/reference/chat/ChatMessage.ipynb) | Display a chat message |
+| [`chat_message`](https://docs.streamlit.io/library/api-reference/chat/st.chat_message) | [`ChatMessage`](../../reference/chat/ChatMessage.md) | Display a chat message |
| [`chat_input`](https://docs.streamlit.io/library/api-reference/chat/st.chat_input) | [`ChatInput` example](https://holoviz-topics.github.io/panel-chat-examples/components/#chat_input) | Input a chat message |
| [`status`](https://docs.streamlit.io/library/api-reference/status/st.status) | [`Status` example](https://holoviz-topics.github.io/panel-chat-examples/components/#status) | Display the output of long-running tasks in a container |
-| | [`ChatFeed`](../../../examples/reference/chat/ChatFeed.ipynb) | Display multiple chat messages |
-| | [`ChatInterface`](../../../examples/reference/chat/ChatInterface.ipynb) | High-level, easy to use chat interface |
-| [`StreamlitCallbackHandler`](https://python.langchain.com/docs/integrations/callbacks/streamlit) | [`PanelCallbackHandler`](../../../examples/reference/chat/PanelCallbackHandler.ipynb) | Display the thoughts and actions of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
+| | [`ChatFeed`](../../reference/chat/ChatFeed.md) | Display multiple chat messages |
+| | [`ChatInterface`](../../reference/chat/ChatInterface.md) | High-level, easy to use chat interface |
+| [`StreamlitCallbackHandler`](https://python.langchain.com/docs/integrations/callbacks/streamlit) | [`PanelCallbackHandler`](../../reference/chat/PanelCallbackHandler.md) | Display the thoughts and actions of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
| [`StreamlitChatMessageHistory`](https://python.langchain.com/docs/integrations/memory/streamlit_chat_message_history) | | Persist the memory of a [LangChain](https://python.langchain.com/docs/get_started/introduction) agent |
-The starting point for most Panel users is the *high-level* [`ChatInterface`](../../../examples/reference/chat/ChatInterface.ipyn) or [`PanelCallbackHandler`](../../../examples/reference/chat/PanelCallbackHandler.ipynb), not the *low-level* [`ChatMessage`](../../../examples/reference/chat/ChatMessage.ipynb) and [`ChatFeed`](../../../examples/reference/chat/ChatFeed.ipynb) components.
+The starting point for most Panel users is the *high-level* [`ChatInterface`](../../reference/chat/ChatInterface.md) or [`PanelCallbackHandler`](../../reference/chat/PanelCallbackHandler.md), not the *low-level* [`ChatMessage`](../../reference/chat/ChatMessage.md) and [`ChatFeed`](../../reference/chat/ChatFeed.md) components.
## Chat Message
diff --git a/doc/how_to/streamlit_migration/panes.md b/doc/how_to/streamlit_migration/panes.md
index d6806408699..f83a6c108c1 100644
--- a/doc/how_to/streamlit_migration/panes.md
+++ b/doc/how_to/streamlit_migration/panes.md
@@ -5,7 +5,7 @@ In Panel the objects that can display your Python objects are called *panes*. Wi
- Get notifications about interactions like click events on your plots and tables and react to them.
- Use unique data visualization ecosystems like HoloViz, ipywidgets and VTK.
-Check out the [Panes Section](../../reference/index#panes) of the [Component Gallery](../../reference/index.md) for the full list of *panes*.
+Check out the [Panes Section](../../reference/index.md#panes) of the [Component Gallery](../../reference/index.md) for the full list of *panes*.
---
diff --git a/doc/how_to/styling/altair.md b/doc/how_to/styling/altair.md
index 3f67c6957fc..54cfc568360 100644
--- a/doc/how_to/styling/altair.md
+++ b/doc/how_to/styling/altair.md
@@ -1,6 +1,6 @@
# Style Altair Plots
-This guide addresses how to style Altair plots displayed using the [Vega pane](../../../examples/reference/panes/Vega).
+This guide addresses how to style Altair plots displayed using the [Vega pane](../../reference/panes/Vega.md).
You can select the theme of Altair plots using [`altair.themes.enable`](https://altair-viz.github.io/user_guide/customization.html#changing-the-theme) and an accent color using the `configure_mark` method. The list of themes is available via `altair.themes.names()`.
diff --git a/examples/apps/django/requirements.txt b/examples/apps/django/requirements.txt
index 59fc2ed647b..58724108f00 100644
--- a/examples/apps/django/requirements.txt
+++ b/examples/apps/django/requirements.txt
@@ -2,4 +2,4 @@ django==2.2.28
channels==2.2.0
panel==0.9.3
bokeh==2.0.2
-jinja2==3.0.1
+jinja2==3.1.3
diff --git a/examples/apps/django_multi_apps/requirements.txt b/examples/apps/django_multi_apps/requirements.txt
index dea92220fa2..fd6f5c5a3df 100644
--- a/examples/apps/django_multi_apps/requirements.txt
+++ b/examples/apps/django_multi_apps/requirements.txt
@@ -16,7 +16,7 @@ hvplot==0.5.2
hyperlink==20.0.1
idna==2.8
incremental==21.3.0
-Jinja2==2.11.3
+Jinja2==3.1.3
Markdown==3.1.1
MarkupSafe==1.1.1
numpy==1.22.0
diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb
index 3c03d464d67..0904ad03d11 100644
--- a/examples/reference/chat/ChatFeed.ipynb
+++ b/examples/reference/chat/ChatFeed.ipynb
@@ -8,7 +8,6 @@
},
"outputs": [],
"source": [
- "import time\n",
"import panel as pn\n",
"\n",
"pn.extension()"
@@ -44,7 +43,7 @@
"##### Styling\n",
"\n",
"* **`card_params`** (Dict): Parameters to pass to Card, such as `header`, `header_background`, `header_color`, etc.\n",
- "* **`message_params`** (Dict): Parameters to pass to each ChatMessage, such as `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, and `show_timestamp`.\n",
+ "* **`message_params`** (Dict): Parameters to pass to each ChatMessage, such as `reaction_icons`, `timestamp_format`, `show_avatar`, `show_user`, and `show_timestamp` Params passed that are not ChatFeed params will be forwarded into `message_params`.\n",
"\n",
"##### Other\n",
"\n",
@@ -55,6 +54,7 @@
"* **`placeholder_threshold`** (float): Min duration in seconds of buffering before displaying the placeholder. If 0, the placeholder will be disabled. Defaults to 0.2.\n",
"* **`auto_scroll_limit`** (int): Max pixel distance from the latest object in the Column to activate automatic scrolling upon update. Setting to 0 disables auto-scrolling.\n",
"* **`scroll_button_threshold`** (int): Min pixel distance from the latest object in the Column to display the scroll button. Setting to 0 disables the scroll button.\n",
+ "* **`show_activity_dot`** (bool): Whether to show an activity dot on the ChatMessage while streaming the callback response.\n",
"* **`view_latest`** (bool): Whether to scroll to the latest object on init. If not enabled the view will be on the first object. Defaults to True.\n",
"\n",
"#### Methods\n",
@@ -604,6 +604,30 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The messages can be filtered by using a custom `filter_by` function."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def filter_by_reactions(messages):\n",
+ " return [message for message in messages if \"favorite\" in message.reactions]\n",
+ "\n",
+ "\n",
+ "chat_feed.send(\n",
+ " pn.chat.ChatMessage(\"I'm a message with a reaction!\", reactions=[\"favorite\"])\n",
+ ")\n",
+ "\n",
+ "chat_feed.serialize(filter_by=filter_by_reactions)"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -716,7 +740,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "You can pass `ChatEntry` params through `entry_params`."
+ "You can pass `ChatEntry` params through `message_params`."
]
},
{
@@ -734,6 +758,25 @@
"chat_feed"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Alternatively, directly pass those params to the ChatFeed constructor and it'll be forwarded into `message_params` automatically."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "chat_feed = pn.chat.ChatFeed(default_avatars={\"System\": \"S\", \"User\": \"👤\"}, reaction_icons={\"like\": \"thumb-up\"})\n",
+ "chat_feed.send(user=\"System\", value=\"This is the System speaking.\")\n",
+ "chat_feed.send(user=\"User\", value=\"This is the User speaking.\")\n",
+ "chat_feed"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
diff --git a/examples/reference/chat/ChatMessage.ipynb b/examples/reference/chat/ChatMessage.ipynb
index 4f5b485e97d..6fb9e99e73f 100644
--- a/examples/reference/chat/ChatMessage.ipynb
+++ b/examples/reference/chat/ChatMessage.ipynb
@@ -59,6 +59,7 @@
"* **`show_timestamp`** (bool): Whether to display the timestamp of the message.\n",
"* **`show_reaction_icons`** (bool): Whether to display the reaction icons.\n",
"* **`show_copy_icon`** (bool): Whether to show the copy icon.\n",
+ "* **`show_activity_dot`** (bool): Whether to show the activity dot.\n",
"* **`name`** (str): The title or name of the chat message widget, if any.\n",
"\n",
"___"
diff --git a/examples/reference/templates/Material.ipynb b/examples/reference/templates/Material.ipynb
index 148bcbc5cde..81211d64d68 100644
--- a/examples/reference/templates/Material.ipynb
+++ b/examples/reference/templates/Material.ipynb
@@ -82,7 +82,7 @@
" )\n",
")\n",
"\n",
- "template.show();"
+ "template.servable();"
]
},
{
diff --git a/examples/reference/widgets/TextAreaInput.ipynb b/examples/reference/widgets/TextAreaInput.ipynb
index b9a990b5681..7371245083e 100644
--- a/examples/reference/widgets/TextAreaInput.ipynb
+++ b/examples/reference/widgets/TextAreaInput.ipynb
@@ -39,6 +39,7 @@
"* **`name`** (str): The title of the widget\n",
"* **`placeholder`** (str): A placeholder string displayed when no value is entered\n",
"* **`rows`** (int, default=2): The number of rows in the text input field. \n",
+ "* **`resizable`** (boolean | str): Whether the layout is interactively resizable, and if so in which dimensions: `width`, `height`, or `both`.\n",
"\n",
"___"
]
diff --git a/examples/reference/widgets/ToggleIcon.ipynb b/examples/reference/widgets/ToggleIcon.ipynb
index 839c5f908ab..b8ee06a06d1 100644
--- a/examples/reference/widgets/ToggleIcon.ipynb
+++ b/examples/reference/widgets/ToggleIcon.ipynb
@@ -25,7 +25,7 @@
"##### Core\n",
"\n",
"* **`active_icon`** (str): The name of the icon to display when toggled from [tabler-icons.io](https://tabler-icons.io)/\n",
- "* **`icon`** (str): The name of the icon to display from [tabler-icons.io](https://tabler-icons.io)/\n",
+ "* **`icon`** (str): The name of the icon to display from [tabler-icons.io](https://tabler-icons.io)/ or an SVG.\n",
"* **`value`** (boolean): Whether the icon is toggled on or off\n",
"\n",
"##### Display\n",
@@ -113,6 +113,29 @@
"pn.widgets.ToggleIcon(icon=\"thumb-down\", active_icon=\"thumb-up\", size='3em')"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You can also use SVGs for icons."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "SVG = \"\"\"\n",
+ "\n",
+ "\"\"\"\n",
+ "ACTIVE_SVG = \"\"\"\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "pn.widgets.ToggleIcon(icon=SVG, active_icon=ACTIVE_SVG, size='3em')"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
diff --git a/lite/files/Getting_Started.ipynb b/lite/files/Getting_Started.ipynb
index 65b644444b8..004e861abd4 100644
--- a/lite/files/Getting_Started.ipynb
+++ b/lite/files/Getting_Started.ipynb
@@ -87,7 +87,7 @@
").properties(\n",
" width=250,\n",
" height=250\n",
- ").add_selection(\n",
+ ").add_params(\n",
" brush\n",
")\n",
"\n",
diff --git a/panel/chat/feed.py b/panel/chat/feed.py
index 2f048d01e44..0cfff58d09c 100644
--- a/panel/chat/feed.py
+++ b/panel/chat/feed.py
@@ -163,7 +163,8 @@ class ChatFeed(ListPanel):
message_params = param.Dict(default={}, doc="""
Params to pass to each ChatMessage, like `reaction_icons`, `timestamp_format`,
- `show_avatar`, `show_user`, and `show_timestamp`.""")
+ `show_avatar`, `show_user`, and `show_timestamp`. Params passed
+ that are not ChatFeed params will be forwarded into `message_params`.""")
header = param.Parameter(doc="""
The header of the chat feed; commonly used for the title.
@@ -197,6 +198,10 @@ class ChatFeed(ListPanel):
display the scroll button. Setting to 0
disables the scroll button.""")
+ show_activity_dot = param.Boolean(default=True, doc="""
+ Whether to show an activity dot on the ChatMessage while
+ streaming the callback response.""")
+
view_latest = param.Boolean(default=True, doc="""
Whether to scroll to the latest object on init. If not
enabled the view will be on the first object.""")
@@ -205,9 +210,6 @@ class ChatFeed(ListPanel):
The placeholder wrapped in a ChatMessage object;
primarily to prevent recursion error in _update_placeholder.""")
- _callback_future = param.ClassSelector(class_=asyncio.Future, allow_None=True, doc="""
- The current, cancellable async task being executed.""")
-
_callback_state = param.ObjectSelector(objects=list(CallbackState), doc="""
The current state of the callback.""")
@@ -217,10 +219,20 @@ class ChatFeed(ListPanel):
_stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/chat_feed.css"]
def __init__(self, *objects, **params):
+ self._callback_future = None
+
if params.get("renderers") and not isinstance(params["renderers"], list):
params["renderers"] = [params["renderers"]]
if params.get("width") is None and params.get("sizing_mode") is None:
params["sizing_mode"] = "stretch_width"
+
+ # forward message params to ChatMessage for convenience
+ message_params = params.get("message_params", {})
+ for param_key in list(params.keys()):
+ if param_key not in ChatFeed.param and param_key in ChatMessage.param:
+ message_params[param_key] = params.pop(param_key)
+ params["message_params"] = message_params
+
super().__init__(*objects, **params)
# instantiate the card's column) is not None)
@@ -241,10 +253,8 @@ def __init__(self, *objects, **params):
stylesheets=self._stylesheets,
**linked_params
)
- self.link(self._chat_log, objects='objects', bidirectional=True)
- # we have a card for the title
- self._card = Card(
- self._chat_log, VSpacer(),
+ card_params = linked_params.copy()
+ card_params.update(
margin=self.param.margin,
align=self.param.align,
header=self.header,
@@ -258,7 +268,14 @@ def __init__(self, *objects, **params):
title_css_classes=["chat-feed-title"],
styles={"padding": "0px"},
stylesheets=self._stylesheets + self.param.stylesheets.rx(),
- **linked_params
+ )
+ card_params.update(self.card_params)
+ self.link(self._chat_log, objects='objects', bidirectional=True)
+ # we have a card for the title
+ self._card = Card(
+ self._chat_log,
+ VSpacer(),
+ **card_params
)
# handle async callbacks using this trick
@@ -269,14 +286,25 @@ def _get_model(
self, doc: Document, root: Model | None = None,
parent: Model | None = None, comm: Comm | None = None
) -> Model:
- return self._card._get_model(doc, root, parent, comm)
+ model = self._card._get_model(doc, root, parent, comm)
+ ref = (root or model).ref['id']
+ self._models[ref] = (model, parent)
+ return model
def _cleanup(self, root: Model | None = None) -> None:
self._card._cleanup(root)
super()._cleanup(root)
+ @param.depends("card_params", watch=True)
+ def _update_card_params(self):
+ self._card.param.update(**self.card_params)
+
@param.depends("placeholder_text", watch=True, on_init=True)
def _update_placeholder(self):
+ if self._placeholder is not None:
+ self._placeholder.param.update(object=self.placeholder_text)
+ return
+
loading_avatar = SVG(
PLACEHOLDER_SVG, sizing_mode=None, css_classes=["rotating-placeholder"]
)
@@ -335,6 +363,7 @@ def _build_message(
message_params["avatar"] = avatar
if self.width:
message_params["width"] = int(self.width - 80)
+
message = ChatMessage(**message_params)
return message
@@ -402,18 +431,24 @@ async def _serialize_response(self, response: Any) -> ChatMessage | None:
updating the message's value.
"""
response_message = None
- if isasyncgen(response):
- self._callback_state = CallbackState.GENERATING
- async for token in response:
- response_message = self._upsert_message(token, response_message)
- elif isgenerator(response):
- self._callback_state = CallbackState.GENERATING
- for token in response:
- response_message = self._upsert_message(token, response_message)
- elif isawaitable(response):
- response_message = self._upsert_message(await response, response_message)
- else:
- response_message = self._upsert_message(response, response_message)
+ try:
+ if isasyncgen(response):
+ self._callback_state = CallbackState.GENERATING
+ async for token in response:
+ response_message = self._upsert_message(token, response_message)
+ response_message.show_activity_dot = self.show_activity_dot
+ elif isgenerator(response):
+ self._callback_state = CallbackState.GENERATING
+ for token in response:
+ response_message = self._upsert_message(token, response_message)
+ response_message.show_activity_dot = self.show_activity_dot
+ elif isawaitable(response):
+ response_message = self._upsert_message(await response, response_message)
+ else:
+ response_message = self._upsert_message(response, response_message)
+ finally:
+ if response_message:
+ response_message.show_activity_dot = False
return response_message
async def _schedule_placeholder(
@@ -429,7 +464,7 @@ async def _schedule_placeholder(
return
start = asyncio.get_event_loop().time()
- while not task.done() and num_entries == len(self._chat_log):
+ while not self._callback_state == CallbackState.IDLE and num_entries == len(self._chat_log):
duration = asyncio.get_event_loop().time() - start
if duration > self.placeholder_threshold:
self.append(self._placeholder)
@@ -659,6 +694,7 @@ def clear(self) -> List[Any]:
def _serialize_for_transformers(
self,
+ messages: List[ChatMessage],
role_names: Dict[str, str | List[str]] | None = None,
default_role: str | None = "assistant",
custom_serializer: Callable = None
@@ -681,8 +717,8 @@ def _serialize_for_transformers(
for name in names:
names_role[name.lower()] = role
- messages = []
- for message in self._chat_log.objects:
+ serialized_messages = []
+ for message in messages:
lowercase_name = message.user.lower()
if lowercase_name not in names_role and not default_role:
@@ -703,11 +739,12 @@ def _serialize_for_transformers(
else:
content = str(message)
- messages.append({"role": role, "content": content})
- return messages
+ serialized_messages.append({"role": role, "content": content})
+ return serialized_messages
def serialize(
self,
+ filter_by: Callable | None = None,
format: Literal["transformers"] = "transformers",
custom_serializer: Callable | None = None,
**serialize_kwargs
@@ -720,10 +757,13 @@ def serialize(
format : str
The format to export the chat log as; currently only
supports "transformers".
+ filter_by : callable
+ A function to filter the chat log by.
+ The function must accept and return a list of ChatMessage objects.
custom_serializer : callable
A custom function to format the ChatMessage's object. The function must
- accept one positional argument. If not provided,
- uses the serialize method on ChatMessage.
+ accept one positional argument, the ChatMessage object, and return a string.
+ If not provided, uses the serialize method on ChatMessage.
**serialize_kwargs
Additional keyword arguments to use for the specified format.
@@ -742,9 +782,13 @@ def serialize(
-------
The chat log serialized in the specified format.
"""
+ messages = self._chat_log.objects.copy()
+ if filter_by is not None:
+ messages = filter_by(messages)
+
if format == "transformers":
return self._serialize_for_transformers(
- custom_serializer=custom_serializer, **serialize_kwargs
+ messages, custom_serializer=custom_serializer, **serialize_kwargs
)
raise NotImplementedError(f"Format {format!r} is not supported.")
diff --git a/panel/chat/interface.py b/panel/chat/interface.py
index 3ef6e68e775..8642dc4e25b 100644
--- a/panel/chat/interface.py
+++ b/panel/chat/interface.py
@@ -23,7 +23,7 @@
from ..widgets.button import Button
from ..widgets.input import FileInput, TextInput
from .feed import CallbackState, ChatFeed
-from .message import _FileInputMessage
+from .message import ChatMessage, _FileInputMessage
@dataclass
@@ -292,8 +292,8 @@ def _init_widgets(self):
sizing_mode="stretch_width",
max_width=show_expr.rx.where(90, 45),
max_height=50,
- margin=(5, 5, 5, 0),
- align="start",
+ margin=(0, 5, 0, 0),
+ align="center",
visible=visible
)
if action != "stop":
@@ -543,6 +543,7 @@ def active(self, index: int) -> None:
def _serialize_for_transformers(
self,
+ messages: List[ChatMessage],
role_names: Dict[str, str | List[str]] | None = None,
default_role: str | None = "assistant",
custom_serializer: Callable = None
@@ -552,6 +553,8 @@ def _serialize_for_transformers(
Arguments
---------
+ messages : list(ChatMessage)
+ A list of ChatMessage objects to serialize.
role_names : dict(str, str | list(str)) | None
A dictionary mapping the role to the ChatMessage's user name.
Defaults to `{"user": [self.user], "assistant": [self.callback_user]}`
@@ -563,8 +566,8 @@ def _serialize_for_transformers(
If this is set to None, raises a ValueError if the user name is not found.
custom_serializer : callable
A custom function to format the ChatMessage's object. The function must
- accept one positional argument and return a string. If not provided,
- uses the serialize method on ChatMessage.
+ accept one positional argument, the ChatMessage object, and return a string.
+ If not provided, uses the serialize method on ChatMessage.
Returns
-------
@@ -575,7 +578,7 @@ def _serialize_for_transformers(
"user": [self.user],
"assistant": [self.callback_user],
}
- return super()._serialize_for_transformers(role_names, default_role, custom_serializer)
+ return super()._serialize_for_transformers(messages, role_names, default_role, custom_serializer)
@param.depends("_callback_state", watch=True)
async def _update_input_disabled(self):
diff --git a/panel/chat/message.py b/panel/chat/message.py
index 9996c5a5b9f..2b4f7c02ce4 100644
--- a/panel/chat/message.py
+++ b/panel/chat/message.py
@@ -197,6 +197,9 @@ class ChatMessage(PaneBase):
show_copy_icon = param.Boolean(default=True, doc="""
Whether to display the copy icon.""")
+ show_activity_dot = param.Boolean(default=False, doc="""
+ Whether to show the activity dot.""")
+
renderers = param.HookList(doc="""
A callable or list of callables that accept the object and return a
Panel object to render the object. If a list is provided, will
@@ -222,7 +225,7 @@ def __init__(self, object=None, **params):
tz = params.get("timestamp_tz")
if tz is not None:
tz = ZoneInfo(tz)
- elif state.browser_info.timezone:
+ elif state.browser_info and state.browser_info.timezone:
tz = ZoneInfo(state.browser_info.timezone)
params["timestamp"] = datetime.datetime.now(tz=tz)
reaction_icons = params.get("reaction_icons", {"favorite": "heart"})
@@ -240,6 +243,9 @@ def __init__(self, object=None, **params):
self._build_layout()
def _build_layout(self):
+ self._activity_dot = HTML(
+ "●", css_classes=["activity-dot"], visible=self.param.show_activity_dot
+ )
self._left_col = left_col = Column(
self._render_avatar(),
max_width=60,
@@ -275,8 +281,10 @@ def _build_layout(self):
Row(
self._user_html,
self.chat_copy_icon,
+ self._activity_dot,
stylesheets=self._stylesheets,
sizing_mode="stretch_width",
+ css_classes=["header"]
),
self._center_row,
self._timestamp_html,
diff --git a/panel/command/serve.py b/panel/command/serve.py
index bcff57c5ccc..de857a3c874 100644
--- a/panel/command/serve.py
+++ b/panel/command/serve.py
@@ -579,7 +579,7 @@ def customize_kwargs(self, args, server_kwargs):
raise ValueError("OAuth encryption key was not a valid base64 "
"string. Generate an encryption key with "
"`panel oauth-secret` and ensure you did not "
- "truncate the returned string.")
+ "truncate the returned string.") from None
if len(key) != 32:
raise ValueError(
"OAuth encryption key must be 32 url-safe "
@@ -605,7 +605,7 @@ def customize_kwargs(self, args, server_kwargs):
"Using OAuth2 provider with Panel requires the "
"cryptography library. Install it with `pip install "
"cryptography` or `conda install cryptography`."
- )
+ ) from None
state.encryption = Fernet(config.oauth_encryption_key)
kwargs['auth_provider'] = OAuthProvider(
diff --git a/panel/compiler.py b/panel/compiler.py
index b3cae33e17b..06270470421 100644
--- a/panel/compiler.py
+++ b/panel/compiler.py
@@ -47,7 +47,7 @@ def write_bundled_files(name, files, explicit_dir=None, ext=None):
except Exception as e:
raise ConnectionError(
f"Failed to fetch {name} dependency: {bundle_file}. Errored with {e}."
- )
+ ) from e
try:
map_file = f'{bundle_file}.map'
map_response = requests.get(map_file)
diff --git a/panel/config.py b/panel/config.py
index b4b26cea0d4..a1909cbf3ef 100644
--- a/panel/config.py
+++ b/panel/config.py
@@ -798,7 +798,7 @@ def __call__(self, *args, **params):
backend = hv.Store.current_backend
else:
backend = 'bokeh'
- if hasattr(hv.Store, 'set_current_backend'):
+ if not loaded or (loaded and backend != hv.Store.current_backend) and hasattr(hv.Store, 'set_current_backend'):
hv.Store.set_current_backend(backend)
else:
hv.Store.current_backend = backend
@@ -911,9 +911,9 @@ def _apply_signatures(self):
parameters = sig_params[:-1]
processed_kws, keyword_groups = set(), []
- for cls in reversed(cls.mro()):
+ for scls in reversed(cls.mro()):
keyword_group = []
- for (k, v) in sorted(cls.__dict__.items()):
+ for (k, v) in sorted(scls.__dict__.items()):
if (isinstance(v, param.Parameter) and k not in processed_kws
and not v.readonly):
keyword_group.append(k)
diff --git a/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css b/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css
index 61663d96117..7287073ae06 100644
--- a/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css
+++ b/panel/dist/bundled/datatabulator/tabulator-tables@5.5.0/dist/css/tabulator_fast.min.css
@@ -566,6 +566,12 @@
background-color: var(--accent-fill-hover);
cursor: pointer;
}
+.tabulator-row.tabulator-selected a {
+ color: var(--foreground-on-accent-rest);
+}
+.tabulator-row.tabulator-selectable:hover a {
+ color: var(--foreground-on-accent-rest);
+}
.tabulator-row.tabulator-row-moving {
border: 1px solid #000;
background: #fff;
diff --git a/panel/dist/css/button.css b/panel/dist/css/button.css
index d2c902a9c50..5ae415e8132 100644
--- a/panel/dist/css/button.css
+++ b/panel/dist/css/button.css
@@ -139,7 +139,7 @@
.bk-btn a {
align-items: center;
- display: flex;
+ display: inline;
height: 100%;
justify-content: center;
padding: 6px;
diff --git a/panel/dist/css/chat_message.css b/panel/dist/css/chat_message.css
index 692dcc4cfa8..fe39d2ea1cc 100644
--- a/panel/dist/css/chat_message.css
+++ b/panel/dist/css/chat_message.css
@@ -47,6 +47,10 @@
max-width: calc(100% - 80px);
}
+.header {
+ width: fit-content;
+}
+
.name {
font-size: 1em;
margin-bottom: 0px;
@@ -67,15 +71,19 @@
color-mix(in srgb, var(--panel-shadow-color) 15%, transparent) 0px 1px 3px
1px;
font-size: 1.25em;
- min-width: 50px;
min-height: 50px;
margin-block: 0px;
margin-left: 10px; /* Space for avatar */
margin-right: 5px; /* Space for reaction */
background-color: var(--panel-surface-color, #f1f1f1);
+ min-width: 0;
max-width: calc(100% - 40px);
padding: 5px;
width: fit-content;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow-wrap: anywhere;
}
.footer {
@@ -88,7 +96,7 @@
.timestamp {
color: #a9a9a9;
display: flex;
- margin-top: 0px;
+ margin-top: 3px;
}
.markdown {
@@ -108,3 +116,23 @@
margin-block: 0px;
margin-inline: 2px;
}
+
+@keyframes fadeOut {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+.activity-dot {
+ display: inline-block;
+ animation: fadeOut 2s infinite cubic-bezier(0.68, -0.55, 0.27, 1.55);
+ color: #32cd32;
+ font-size: 1.25em;
+ margin-block: 0px;
+}
diff --git a/panel/dist/css/loading.css b/panel/dist/css/loading.css
index 80294a3bf74..6dbd45299e8 100644
--- a/panel/dist/css/loading.css
+++ b/panel/dist/css/loading.css
@@ -21,6 +21,8 @@
:host(.pn-loading) .pn-loading-msg,
.pn-loading .pn-loading-msg {
+ color: var(--panel-on-background-color, black);
+ font-size: 2em;
position: absolute;
top: 72%;
font-size: 2em;
diff --git a/panel/dist/css/widgetbox.css b/panel/dist/css/widgetbox.css
index 56ec2f00fe4..a20fcf02e4c 100644
--- a/panel/dist/css/widgetbox.css
+++ b/panel/dist/css/widgetbox.css
@@ -4,6 +4,4 @@
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
- overflow-x: hidden;
- overflow-y: hidden;
}
diff --git a/panel/io/cache.py b/panel/io/cache.py
index 6927c7e0428..5a3016dbeb7 100644
--- a/panel/io/cache.py
+++ b/panel/io/cache.py
@@ -192,14 +192,14 @@ def _generate_hash_inner(obj):
raise ValueError(
f'User hash function {hash_func!r} failed for input '
f'{obj!r} with following error: {type(e).__name__}("{e}").'
- )
+ ) from e
return output
if hasattr(obj, '__reduce__'):
h = hashlib.new("md5")
try:
reduce_data = obj.__reduce__()
except BaseException:
- raise ValueError(f'Could not hash object of type {type(obj).__name__}')
+ raise ValueError(f'Could not hash object of type {type(obj).__name__}') from None
for item in reduce_data:
h.update(_generate_hash(item))
return h.digest()
@@ -319,6 +319,9 @@ def cache(
A dictionary mapping from a type to a function which returns
a hash for an object of that type. If provided this will
override the default hashing function provided by Panel.
+ max_items: int or None
+ The maximum items to keep in the cache. Default is None, which does
+ not limit number of items stored in the cache.
policy: str
A caching policy when max_items is set, must be one of:
- FIFO: First in - First out
@@ -347,7 +350,8 @@ def cache(
max_items=max_items,
ttl=ttl,
to_disk=to_disk,
- cache_path=cache_path
+ cache_path=cache_path,
+ per_session=per_session,
)
func_hash = None # noqa
@@ -431,7 +435,7 @@ def wrapped_func(*args, **kwargs):
func_cache[hash_value] = (ret, time, 0, time)
return ret
- def clear(session_context=None):
+ def clear():
global func_hash
# clear called before anything is cached.
if 'func_hash' not in globals():
@@ -445,10 +449,13 @@ def clear(session_context=None):
else:
cache = state._memoize_cache.get(func_hash, {})
cache.clear()
+
wrapped_func.clear = clear
if per_session and state.curdoc and state.curdoc.session_context:
- state.curdoc.on_session_destroyed(clear)
+ def server_clear(session_context):
+ clear()
+ state.curdoc.on_session_destroyed(server_clear)
try:
wrapped_func.__dict__.update(func.__dict__)
diff --git a/panel/io/convert.py b/panel/io/convert.py
index bf82a4f93d2..c3e9fb48154 100644
--- a/panel/io/convert.py
+++ b/panel/io/convert.py
@@ -248,7 +248,7 @@ def script_to_html(
except Exception as e:
raise ValueError(
f'Requirements parser raised following error: {e}'
- )
+ ) from e
# Environment
if panel_version == 'local':
diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py
index 9bc5d349b01..8ea0a26383d 100644
--- a/panel/io/jupyter_server_extension.py
+++ b/panel/io/jupyter_server_extension.py
@@ -169,9 +169,9 @@ async def _get_info(self, msg_id, timeout=KERNEL_TIMEOUT):
raise TimeoutError('Timed out while waiting for kernel to open Comm channel to Panel application.')
try:
msg = await ensure_async(self.kernel.iopub_channel.get_msg(timeout=None))
- except Empty:
+ except Empty as e:
if not await ensure_async(self.kernel.is_alive()):
- raise RuntimeError("Kernel died before establishing Comm connection to Panel application.")
+ raise RuntimeError("Kernel died before establishing Comm connection to Panel application.") from e
continue
if msg['parent_header'].get('msg_id') != msg_id:
continue
@@ -381,9 +381,9 @@ async def _check_for_message(self):
break
try:
msg = await ensure_async(self.kernel.iopub_channel.get_msg(timeout=None))
- except Empty:
+ except Empty as e:
if not await ensure_async(self.kernel.is_alive()):
- raise RuntimeError("Kernel died before expected shutdown of Panel app.")
+ raise RuntimeError("Kernel died before expected shutdown of Panel app.") from e
continue
msg_type = msg['header']['msg_type']
diff --git a/panel/io/liveness.py b/panel/io/liveness.py
index 3f65b270a95..e0975db2605 100644
--- a/panel/io/liveness.py
+++ b/panel/io/liveness.py
@@ -25,4 +25,4 @@ async def get(self):
except Exception as e:
raise web.HTTPError(
500, f"Endpoint {endpoint!r} could not be served. Application raised error: {e}"
- )
+ ) from e
diff --git a/panel/io/model.py b/panel/io/model.py
index 44e6606d35b..5fbb5d1757f 100644
--- a/panel/io/model.py
+++ b/panel/io/model.py
@@ -160,8 +160,8 @@ def bokeh_repr(obj: Model, depth: int = 0, ignored: Optional[Iterable[str]] = No
props_repr = ', '.join(props)
if isinstance(obj, FlexBox):
r += '{cls}(children=[\n'.format(cls=cls)
- for obj in obj.children: # type: ignore
- r += textwrap.indent(bokeh_repr(obj, depth=depth+1) + ',\n', ' ')
+ for child_obj in obj.children: # type: ignore
+ r += textwrap.indent(bokeh_repr(child_obj, depth=depth+1) + ',\n', ' ')
r += '], %s)' % props_repr
else:
r += '{cls}({props})'.format(cls=cls, props=props_repr)
diff --git a/panel/io/notebook.py b/panel/io/notebook.py
index c09ec4ce887..61c12766c34 100644
--- a/panel/io/notebook.py
+++ b/panel/io/notebook.py
@@ -8,9 +8,11 @@
import os
import sys
import uuid
+import warnings
from collections import OrderedDict
from contextlib import contextmanager
+from functools import partial
from typing import (
TYPE_CHECKING, Any, Dict, Iterator, List, Literal, Optional, Tuple,
)
@@ -63,13 +65,31 @@
def _jupyter_server_extension_paths() -> List[Dict[str, str]]:
return [{"module": "panel.io.jupyter_server_extension"}]
-def push(doc: 'Document', comm: 'Comm', binary: bool = True) -> None:
+def push(doc: Document, comm: Comm, binary: bool = True, msg: any = None) -> None:
"""
Pushes events stored on the document across the provided comm.
"""
- msg = diff(doc, binary=binary)
+ if msg is None:
+ msg = diff(doc, binary=binary)
if msg is None:
return
+ elif not comm._comm:
+ try:
+ from tornado.ioloop import IOLoop
+ IOLoop.current().call_later(0.1, partial(push, doc, comm, binary, msg=msg))
+ except Exception:
+ warnings.warn(
+ 'Attempted to send message over Jupyter Comm but it was not '
+ 'yet open and also could not be rescheduled to a later time. '
+ 'The update will not be sent.', UserWarning, stacklevel=0
+ )
+ else:
+ send(comm, msg)
+
+def send(comm: Comm, msg: any):
+ """
+ Sends a bokeh message across a pyviz_comms.Comm.
+ """
# WARNING: CommManager model assumes that either JSON content OR a buffer
# is sent. Therefore we must NEVER(!!!) send both at once.
comm.send(msg.header_json)
diff --git a/panel/io/rest.py b/panel/io/rest.py
index 0a76532ed77..0bd052315c7 100644
--- a/panel/io/rest.py
+++ b/panel/io/rest.py
@@ -152,7 +152,7 @@ def param_rest_provider(files, endpoint):
try:
import nbconvert # noqa
except ImportError:
- raise ImportError("Please install nbconvert to serve Jupyter Notebooks.")
+ raise ImportError("Please install nbconvert to serve Jupyter Notebooks.") from None
from nbconvert import ScriptExporter
exporter = ScriptExporter()
source, _ = exporter.from_filename(filename)
diff --git a/panel/io/server.py b/panel/io/server.py
index 7c947f0dcf0..dc9f01b9a52 100644
--- a/panel/io/server.py
+++ b/panel/io/server.py
@@ -704,11 +704,11 @@ def parse_url_path(self, path: str) -> str:
try:
module = importlib.import_module(mod)
except ModuleNotFoundError:
- raise HTTPError(404, 'Module not found')
+ raise HTTPError(404, 'Module not found') from None
try:
component = getattr(module, cls)
except AttributeError:
- raise HTTPError(404, 'Component not found')
+ raise HTTPError(404, 'Component not found') from None
# May only access resources listed in specific attributes
if rtype not in self._resource_attrs:
@@ -717,7 +717,7 @@ def parse_url_path(self, path: str) -> str:
try:
resources = getattr(component, rtype)
except AttributeError:
- raise HTTPError(404, 'Resource type not found')
+ raise HTTPError(404, 'Resource type not found') from None
# Handle template resources
if rtype == '_resources':
@@ -1174,7 +1174,7 @@ def get_server(
raise KeyError(
"Keys of the title dictionary and of the apps "
f"dictionary must match. No {slug} key found in the "
- "title dictionary.")
+ "title dictionary.") from None
else:
title_ = title
slug = slug if slug.startswith('/') else '/'+slug
diff --git a/panel/io/state.py b/panel/io/state.py
index 09b60a8e429..6336b107510 100644
--- a/panel/io/state.py
+++ b/panel/io/state.py
@@ -215,7 +215,7 @@ class _state(param.Parameterized):
def __repr__(self) -> str:
server_info = []
- for server, panel, docs in self._servers.values():
+ for server, panel, _docs in self._servers.values():
server_info.append(
"{}:{:d} - {!r}".format(server.address or "localhost", server.port, panel)
)
@@ -967,10 +967,11 @@ def curdoc(self) -> Document | None:
pyodide_session = self._is_pyodide and 'pyodide_kernel' not in sys.modules
if doc and (doc.session_context or pyodide_session):
return doc
- finally:
- curdoc = self._curdoc.get()
- if curdoc:
- return curdoc
+ except Exception:
+ pass
+ curdoc = self._curdoc.get()
+ if curdoc:
+ return curdoc
@curdoc.setter
def curdoc(self, doc: Document) -> None:
diff --git a/panel/layout/grid.py b/panel/layout/grid.py
index 728fbaf4202..f8c051cce0b 100644
--- a/panel/layout/grid.py
+++ b/panel/layout/grid.py
@@ -405,7 +405,7 @@ def _yoffset(self):
@property
def _object_grid(self):
grid = np.full((self.nrows, self.ncols), None, dtype=object)
- for i, ((y0, x0, y1, x1), obj) in enumerate(self.objects.items()):
+ for (y0, x0, y1, x1), obj in self.objects.items():
l = 0 if x0 is None else x0
r = self.ncols if x1 is None else x1
t = 0 if y0 is None else y0
diff --git a/panel/layout/gridstack.py b/panel/layout/gridstack.py
index a35d3606a2f..bfb9a62e742 100644
--- a/panel/layout/gridstack.py
+++ b/panel/layout/gridstack.py
@@ -183,7 +183,7 @@ def _update_sizing(self):
else:
height = 0
- for i, ((y0, x0, y1, x1), obj) in enumerate(self.objects.items()):
+ for (y0, x0, y1, x1), obj in self.objects.items():
x0 = 0 if x0 is None else x0
x1 = (self.ncols) if x1 is None else x1
y0 = 0 if y0 is None else y0
diff --git a/panel/links.py b/panel/links.py
index fedf3cb49a9..faf359e1aa1 100644
--- a/panel/links.py
+++ b/panel/links.py
@@ -578,10 +578,10 @@ def _get_triggers(
def _get_specs(
self, link: 'Link', source: 'Reactive', target: 'JSLinkTarget'
) -> Sequence[Tuple['SourceModelSpec', 'TargetModelSpec', str | None]]:
- for src_spec, code in link.code.items():
- src_specs = src_spec.split('.')
- if src_spec.startswith('event:'):
- src_spec = (None, src_spec)
+ for spec in link.code:
+ src_specs = spec.split('.')
+ if spec.startswith('event:'):
+ src_spec = (None, spec)
elif len(src_specs) > 1:
src_spec = ('.'.join(src_specs[:-1]), src_specs[-1])
else:
@@ -589,7 +589,7 @@ def _get_specs(
if isinstance(source, Reactive):
src_prop = source._rename.get(src_prop, src_prop)
src_spec = (None, src_prop)
- return [(src_spec, (None, None), code)]
+ return [(src_spec, (None, None), link.code[spec])]
diff --git a/panel/models/card.ts b/panel/models/card.ts
index 7a546041c7f..8c16f5b7c4c 100644
--- a/panel/models/card.ts
+++ b/panel/models/card.ts
@@ -12,8 +12,7 @@ export class CardView extends ColumnView {
connect_signals(): void {
super.connect_signals()
- const {active_header_background, children, collapsed, header_background, header_color, hide_header} = this.model.properties
- this.on_change(children, () => this.render())
+ const {active_header_background, collapsed, header_background, header_color, hide_header} = this.model.properties
this.on_change(collapsed, () => this._collapse())
this.on_change([header_color, hide_header], () => this.render())
diff --git a/panel/models/file_download.ts b/panel/models/file_download.ts
index 486a19b5291..bdb21f161a5 100644
--- a/panel/models/file_download.ts
+++ b/panel/models/file_download.ts
@@ -90,8 +90,7 @@ export class FileDownloadView extends InputWidgetView {
// 3. auto=True: The widget is a button, i.e right click to "Save as..." won't work
this.anchor_el = document.createElement('a')
this.button_el = button({
- disabled: this.model.disabled,
- type: "bk_btn, bk_btn_type",
+ disabled: this.model.disabled
})
if (this.icon_view != null) {
const separator = this.model.label != "" ? nbsp() : text("")
diff --git a/panel/models/icon.py b/panel/models/icon.py
index 5878a36348e..059af48a0e9 100644
--- a/panel/models/icon.py
+++ b/panel/models/icon.py
@@ -12,7 +12,7 @@ class ToggleIcon(Widget):
The name of the icon to display when toggled.""")
icon = String(default="heart", help="""
- The name of the icon to display.""")
+ The name of the icon or SVG to display.""")
size = String(default="1em", help="""
The size of the icon as a valid CSS font-size.""")
diff --git a/panel/models/icon.ts b/panel/models/icon.ts
index 1b9cf847f2c..911c6c80b2b 100644
--- a/panel/models/icon.ts
+++ b/panel/models/icon.ts
@@ -1,13 +1,14 @@
import { TablerIcon, TablerIconView } from "@bokehjs/models/ui/icons/tabler_icon";
+import { SVGIcon, SVGIconView } from "@bokehjs/models/ui/icons/svg_icon";
import { Control, ControlView } from '@bokehjs/models/widgets/control';
import type { IterViews } from '@bokehjs/core/build_views';
import * as p from "@bokehjs/core/properties";
import { build_view } from '@bokehjs/core/build_views';
-
export class ToggleIconView extends ControlView {
model: ToggleIcon;
- icon_view: TablerIconView;
+ icon_view: TablerIconView | SVGIconView;
+ was_svg_icon: boolean
public *controls() { }
@@ -19,11 +20,8 @@ export class ToggleIconView extends ControlView {
override async lazy_initialize(): Promise {
await super.lazy_initialize();
- const size = this.calculate_size();
- const icon_model = new TablerIcon({ icon_name: this.model.icon, size: size });
- this.icon_view = await build_view(icon_model, { parent: this });
-
- this.icon_view.el.addEventListener('click', () => this.toggle_value());
+ this.was_svg_icon = this.is_svg_icon(this.model.icon)
+ this.icon_view = await this.build_icon_model(this.model.icon, this.was_svg_icon);
}
override *children(): IterViews {
@@ -31,6 +29,10 @@ export class ToggleIconView extends ControlView {
yield this.icon_view;
}
+ is_svg_icon(icon: string): boolean {
+ return icon.trim().startsWith('