holoviz / param

Param: Make your Python code clearer and more reliable by declaring Parameters
https://param.holoviz.org
BSD 3-Clause "New" or "Revised" License
412 stars 69 forks source link

Make it easy to use Traitlets including Ipywidgets/ AnyWidgets with Param #891

Open MarcSkovMadsen opened 7 months ago

MarcSkovMadsen commented 7 months ago

Today you can panel.bind to the value trait of an ipywidgets. But ipywidgets sometimes don't have a value trait or sometimes it has lots of extra, interesting traits.

In https://discourse.holoviz.org/t/panel-works-with-anywidget/6466 I demonstrate how you can easily create an observer, A Parameterized object with parameters corresponding to the ipywidgets that observe for trait changes. Such an observer makes it easy to .watch, @depends, bind and more to the ipywidget.

Please consolidate this functionality and add it to Param.

It should probably be easy to only observe a specified selection of traits (white list) as well as a specified selection of traits not to watch (black list).

# pip install panel ipywidgets_bokeh anywidget
import anywidget
import traitlets

class CounterWidget(anywidget.AnyWidget):
    _esm = """
    export function render({ model, el }) {
      let getCount = () => model.get("count");
      let button = document.createElement("button");
      button.classList.add("counter-button");
      button.innerHTML = `Count is ${getCount()}`;
      button.addEventListener("click", () => {
        model.set("count", getCount() + 1);
        model.save_changes();
      });
      model.on("change:count", () => {
        button.innerHTML = `Count is ${getCount()}`;
      });
      el.appendChild(button);
    }
    """
    _css="""
    .counter-button { background-color: pink; font-size: 48px}
    .counter-button:hover { background-color: pink; }
    """
    count = traitlets.Int(0).tag(sync=True)

counter = CounterWidget()

# HELP FUNCTIONALITY to convert Traitlets Classes/ Events to Param Classes/ Events

import param

_ipywidget_classes = {}
_any_widget_traits = set(anywidget.AnyWidget().traits())

def create_observer(obj, traits=None)->param.Parameterized:
    """Returns a Parameterized class with parameters corresponding to the traits of the obj

    Args:
        traits: A list of traits to observe. If None all traits not on the base AnyWidget will be
        observed.
    """
    if not traits:
        traits = list(set(obj.traits())-_any_widget_traits)
    name = type(obj).__name__
    if name in _ipywidget_classes:
        observer_class = _ipywidget_classes[name]
    else:
        observer_class = param.parameterized_class(name, {trait: param.Parameter() for trait in traits})
        _ipywidget_classes[name] = observer_class

    values = {trait: getattr(obj, trait) for trait in traits}
    observer = observer_class(**values)
    obj.observe(lambda event: setattr(observer, event["name"], event["new"]), names=traits)
    return observer

# THE PANEL APP

import panel as pn
pn.extension("ipywidgets")

observer = create_observer(counter)

def some_output(count):
    return f"The count is {count}!"

component = pn.Column(counter, pn.bind(some_output, observer.param.count))

pn.template.FastListTemplate(
    site="Panel",
    title="Works with AnyWidget",
    main=[component],
).servable()

https://github.com/holoviz/param/assets/42288570/d68bb4dc-d83f-44fd-8e0e-4628f75fc86f

jbednar commented 7 months ago

That would all be great to have!