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 10 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
54 changes: 54 additions & 0 deletions doc/how_to/custom_components/esm/custom_layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This example will show you how to create a *split* layout containing two objects
::::{tab-set}

:::{tab-item} `JSComponent`

```{pyodide}
import panel as pn

Expand Down Expand Up @@ -163,6 +164,7 @@ split_react = SplitReact(
)
split_react.servable()
```

:::

::::
Expand All @@ -172,15 +174,53 @@ Let's verify that the layout will automatically update when the `object` is chan
::::{tab-set}

:::{tab-item} `JSComponent`

```{pyodide}
split_js.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")
```

:::

:::{tab-item} `ReactComponent`

```{pyodide}
split_react.right=pn.pane.Markdown("Hi. I'm a `Markdown` pane replacing the `CodeEditor` widget!", sizing_mode="stretch_both")
```

:::

::::

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",
)
```

:::

::::
Expand All @@ -190,6 +230,7 @@ Now, let's change it back:
::::{tab-set}

:::{tab-item} `JSComponent`

```{pyodide}
split_js.right=pn.widgets.CodeEditor(
value="Right",
Expand All @@ -199,9 +240,11 @@ split_js.right=pn.widgets.CodeEditor(
language="python",
)
```

:::

:::{tab-item} `ReactComponent`

```{pyodide}
split_react.right=pn.widgets.CodeEditor(
value="Right",
Expand All @@ -211,6 +254,7 @@ split_react.right=pn.widgets.CodeEditor(
language="python",
)
```

:::

::::
Expand All @@ -222,6 +266,7 @@ A Panel `Column` or `Row` works as a list of objects. It is *list-like*. In this
::::{tab-set}

:::{tab-item} `JSComponent`

```{pyodide}
import panel as pn
import param
Expand Down Expand Up @@ -298,6 +343,7 @@ You must list `ListLike, JSComponent` in exactly that order when you define the
:::

:::{tab-item} `ReactComponent`

```{pyodide}
import panel as pn
import param
Expand Down Expand Up @@ -362,6 +408,7 @@ grid_react = GridReact(
)
grid_react.servable()
```

:::

::::
Expand All @@ -385,9 +432,11 @@ grid_js.append(
)
)
```

:::

:::{tab-item} `ReactComponent`

```{pyodide}
grid_react.append(
pn.widgets.CodeEditor(
Expand All @@ -397,6 +446,7 @@ grid_react.append(
)
)
```

:::

::::
Expand All @@ -406,15 +456,19 @@ Let's remove it again:
::::{tab-set}

:::{tab-item} `JSComponent`

```{pyodide}
grid_js.pop(-1)
```

:::

:::{tab-item} `ReactComponent`

```{pyodide}
grid_react.pop(-1)
```

:::

::::
188 changes: 188 additions & 0 deletions doc/tutorials/intermediate/create_custom_widget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Build a Custom FeatureInput Widget in Python

Welcome to the "Build a Custom FeatureInput Widget" tutorial! In this guide, we will walk through the process of creating a custom widget that enables users to select a list of features and set their values entirely in Python. This can be particularly useful, for instance, in forecasting the power production of a wind turbine using advanced machine learning models.

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. In this tutorial, we will combine a `MultiSelect` widget with a dynamic number of `FloatInput` widgets to achieve our goal.

## 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"
)

self._layout = pn.Column(selected_features_widget, self._selected_widgets)

def __panel__(self):
return self._layout

@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
```

A key design decision in this implementation is to use the parameter state to manage the widget's internal state and update the layout. The alternative would be to keep the state in the `MultiChoice` and `FloatInput` widgets. This makes the flow easier to reason about.

## 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.

```python
def create_app():
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,
)

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

if pn.state.served:
pn.extension(design="material")

create_app().servable()
```

You can serve the application with `panel serve name_of_file.py`.

## Explanation

### Widget Definition

The `FeatureInput` class inherits from `pn.custom.PyComponent` and `pn.widgets.WidgetBase`. 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.

### Initialization

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. Finally we define the `_layoyt` attribute to hold the sub components of the widget.

### `__panel__`

`PyComponent` classes must define a `__panel__` method which tells Panel how the component should be rendered. Here we simply return the `_layout` we created.

### Parameter Dependencies

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.

### Example Application

The `create_app` function demonstrates how to use the `FeatureInput` widget with a predefined set of features. It returns a layout with the widget and a JSON pane to display the current values.

## Conclusion

In this tutorial, we have learned how to create a custom `FeatureInput` widget using HoloViz Panel's `CompositeWidget`. This custom widget allows users to select features and set their values interactively. Such a widget can be highly useful in various applications, such as configuring parameters for machine learning models or setting up simulations.

Feel free to explore and extend this example to suit your specific needs. Happy coding!

## References

- [CompositeWidget](../../reference/custom_components/CompositeWidget.html)
2 changes: 2 additions & 0 deletions doc/tutorials/intermediate/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Now that you've mastered the more advanced concepts of Panel, it's time to put y
- **Create an Interactive Report:** Elevate the interactivity of your reports through embedding.
- **[Create a Todo App](build_todo.md):** Create a Todo App using a class based approach.
- **[Test a Todo App](test_todo.md):** Learn how to test a class based Panel app.
- **[Create a Custom widget](create_custom_widget.md):** Create a custom `FeatureInput` widget.
- **Serve Apps without a Server:** Explore the realm of WASM to serve your apps without traditional servers.
- **[Build a Server Video Stream](build_server_video_stream.md):** Utilize threading to set up a video stream from a camera connected to a server without blocking the UI.

Expand All @@ -86,5 +87,6 @@ serve
advanced_layouts
build_todo
test_todo
create_custom_widget
build_server_video_stream
```
Loading
Loading