Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PyComponent #7051

Merged
merged 19 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions doc/how_to/custom_components/esm/custom_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,9 @@ split_js = SplitJS(
)
split_js.servable()
```

:::

:::{tab-item} `ReactComponent`

```{pyodide}
import panel as pn

Expand Down Expand Up @@ -215,6 +213,36 @@ split_react.right=pn.widgets.CodeEditor(

::::

Now, let's change it back:

::::{tab-set}

:::{tab-item} `JSComponent`
```{pyodide}
split_js.right=pn.widgets.CodeEditor(
value="Right",
sizing_mode="stretch_both",
margin=0,
theme="monokai",
language="python",
)
```
:::

:::{tab-item} `ReactComponent`
```{pyodide}
split_react.right=pn.widgets.CodeEditor(
value="Right",
sizing_mode="stretch_both",
margin=0,
theme="monokai",
language="python",
)
```
:::

::::

## Layout a List of Objects

A Panel `Column` or `Row` works as a list of objects. It is *list-like*. In this section, we will show you how to create your own *list-like* layout using Panel's `NamedListLike` class.
Expand Down Expand Up @@ -375,7 +403,6 @@ You can now use `[...]` indexing and methods like `.append`, `.insert`, `pop`, e
::::{tab-set}

:::{tab-item} `JSComponent`

```{pyodide}
grid_js.append(
pn.widgets.CodeEditor(
Expand Down
12 changes: 10 additions & 2 deletions doc/how_to/custom_components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

These How-to pages provide solutions for common tasks related to extending Panel with custom components.

## `Viewer` Components
## `Viewer` and `PyComponent`

Build custom components by combining existing components.
Build custom components by combining existing components in Python.

::::{grid} 1 2 2 3
:gutter: 1 1 1 2
Expand All @@ -16,6 +16,13 @@ Build custom components by combining existing components.
How to build custom components that are combinations of existing components.
:::

:::{grid-item-card} {octicon}`pencil;2.5em;sd-mr-1 sd-animate-grow50` Combine Existing Components
:link: python/create_custom widget
:link-type: doc

How to build a custom widget by subclassing `PyComponent` and `WidgetBase`.
:::

::::

### Examples
Expand Down Expand Up @@ -47,6 +54,7 @@ Build a custom component wrapping a table and some widgets using the `Viewer` pa
:maxdepth: 2

custom_viewer
python/create_custom_widget
examples/plot_viewer
examples/table_viewer
```
Expand Down
174 changes: 174 additions & 0 deletions doc/how_to/custom_components/python/create_custom_widget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Build a Widget in Python

In this guide we will demonstrate how to create a custom widget that enables users to select a list of features and set their values entirely in Python.

We will leverage the `PyComponent` class to construct this custom widget. The `PyComponent` allows us to combine multiple Panel components into a more complex and functional widget. The resulting class will combine a `MultiSelect` widget with a dynamic number of `FloatInput` widgets.

## Code Overview

Below is the complete implementation of the `FeatureInput` custom widget:

```{pyodide}
import panel as pn
import param

from panel.widgets.base import WidgetBase
from panel.custom import PyComponent

class FeatureInput(WidgetBase, PyComponent):
"""
The `FeatureInput` enables a user to select from a list of features and set their values.
"""

value = param.Dict(
doc="The names of the features selected and their set values", allow_None=False
)

features = param.Dict(
doc="The names of the available features and their default values"
)

selected_features = param.ListSelector(
doc="The list of selected features"
)

_selected_widgets = param.ClassSelector(
class_=pn.Column, doc="The widgets used to edit the selected features"
)

def __init__(self, **params):
params["value"] = params.get("value", {})
params["features"] = params.get("features", {})
params["selected_features"] = params.get("selected_features", [])

params["_selected_widgets"] = self.param._selected_widgets.class_()

super().__init__(**params)

selected_features_widget = pn.widgets.MultiChoice.from_param(
self.param.selected_features, sizing_mode="stretch_width"
)

def __panel__(self):
return pn.Column(selected_features_widget, self._selected_widgets)

@param.depends("features", watch=True, on_init=True)
def _reset_selected_features(self):
selected_features = []
for feature in self.selected_features.copy():
if feature in self.features.copy():
selected_features.append(feature)

self.param.selected_features.objects = list(self.features)
self.selected_features = selected_features

@param.depends("selected_features", watch=True, on_init=True)
def _handle_selected_features_change(self):
org_value = self.value

self._update_selected_widgets(org_value)
self._update_value()

def _update_value(self, *args): # pylint: disable=unused-argument
new_value = {}

for widget in self._selected_widgets:
new_value[widget.name] = widget.value

self.value = new_value

def _update_selected_widgets(self, org_value):
new_widgets = {}

for feature in self.selected_features:
value = org_value.get(feature, self.features[feature])
widget = self._new_widget(feature, value)
new_widgets[feature] = widget

self._selected_widgets[:] = list(new_widgets.values())

def _new_widget(self, feature, value):
widget = pn.widgets.FloatInput(
name=feature, value=value, sizing_mode="stretch_width"
)
pn.bind(self._update_value, widget, watch=True)
return widget
```

This is a lot to take in so let us break it down into a few pieces:

### Inheritance

The `FeatureInput` class inherits from `pn.custom.PyComponent` and `pn.widgets.WidgetBase`. This multiple inheritance structure allows us to create custom components that behave one of the three core component types that Panel defines `Widget`, `Pane` and `Panel` (i.e. a layout). You should always inherit from the component type base class first, i.e. `WidgetBase` in this case and the component implementation class second, i.e. `PyComponent` in this case.

### Parameter Definitions

It defines the following parameters:

- `value`: A dictionary that stores the selected features and their corresponding values.
- `features`: A dictionary of available features and their default values.
- `selected_features`: The list of features that have been selected.
- `_selected_widgets`: A "private" column layout that contains the widgets for editing the selected features.

### State handling

The two most important methods in configuring state are the constructor (`__init__`) and the (`__panel__`) method which will be invoked to create the component lazily at render time.

#### Constructor

In the `__init__` method, we initialize the widget parameters and create a `MultiChoice` widget for selecting features. We also set up a column to hold the selected feature widgets.

#### `__panel__`

`PyComponent` classes must define a `__panel__` method which tells Panel how the component should be rendered. Here we return a layout of the `MultiSelect` and a column containing the selected features.

#### Syncing state

We use `@param.depends` decorators to define methods that react to changes in the `features` and `selected_features` parameters:

- `_reset_selected_features`: Ensures that only available features are selected.
- `_handle_selected_features_change`: Updates the widgets and the `value` parameter when the selected features change.

#### Widget Updates

The `_update_value` method updates the `value` parameter based on the current values of the feature widgets. The `_update_selected_widgets` method creates and updates the widgets for the selected features.

## Creating the Application

Now, let's create an application to demonstrate our custom `FeatureInput` widget in action. We will define a set of features related to a wind turbine and use our widget to select and set their values:

```{pyodide}
features = {
"Blade Length (m)": 73.5,
"Cut-in Wind Speed (m/s)": 3.5,
"Cut-out Wind Speed (m/s)": 25,
"Grid Connection Capacity (MW)": 5,
"Hub Height (m)": 100,
"Rated Wind Speed (m/s)": 12,
"Rotor Diameter (m)": 150,
"Turbine Efficiency (%)": 45,
"Water Depth (m)": 30,
"Wind Speed (m/s)": 10,
}
selected_features = ["Wind Speed (m/s)", "Rotor Diameter (m)"]
widget = FeatureInput(
features=features,
selected_features=selected_features,
width=500,
)

pn.FlexBox(
pn.Column(
"## Widget",
widget,
),
pn.Column(
"## Value",
pn.pane.JSON(widget.param.value, width=500, height=200),
),
)
```

## References

- [PyComponent](../../reference/custom_components/PyComponent.html)
Loading
Loading