Custom Bokeh models for panel

I am learning how to develop custom Bokeh models for Panel. I found a couple of topics here on Discourse, but somehow it seems the basics are not really covered.
Here in the following the modifications I made to the panel repo:

  • added “panel/panel/models/custom.py”:
from bokeh.core.properties import String
from bokeh.models import UIElement

class Custom(UIElement):

    text = String(default="Custom text")

  • added “panel/panel/models/custom.ts”:
import {UIElement, UIElementView} from "@bokehjs/models/ui/ui_element"
import {div} from "@bokehjs/core/dom"
import * as p from "@bokehjs/core/properties"

export class CustomView extends UIElementView {
  declare model: Custom

  private content_el: HTMLElement

  override render(): void {
    super.render()

    this.content_el = div({style: {
      textAlign: "center",
      fontSize: "1.2em",
      padding: "2px",
      color: "#b88d8e",
      backgroundColor: "#2a3153",
    }})
    this.shadow_el.append(this.content_el)

    this._update_text()
  }

  private _update_text(): void {
    this.content_el.textContent = `${this.model.text}`
  }
}

export namespace Custom {
  export type Attrs = p.AttrsOf<Props>

  export type Props = UIElement.Props & {
    text: p.Property<string>
  }
}

export interface Custom extends Custom.Attrs {}

export class Custom extends UIElement {
  declare properties: Custom.Props
  declare __view_type__: CustomView

  constructor(attrs?: Partial<Custom.Attrs>) {
    super(attrs)
  }

  static {
    this.prototype.default_view = CustomView

    this.define<Custom.Props>(({Str}) => ({
      text:   [ Str, "Custom text" ],
    }))
  }
}

  • added "export {Custom} from “./custom” to “panel/panel/models/index.ts”
  • added “from .custom import Custom” to “panel/panel/models/init.py”
  • added “panel/panel/widgets/custom.py”:
import param

from ..models import Custom as _BkCustom

from typing import ClassVar

from bokeh.model import Model

class Custom:

    text = param.String(default='')

    _widget_type: ClassVar[type[Model]] = _BkCustom

  • added “from .custom import Custom # noqa” to “panel/panel/widgets/init.py”

then I:

pip install -e .
panel bundle --all
pre-commit install

or “panel build panel” if previous commands have already been performed.

The new panel widget is correctly registered, and can be imported in a jupyter notebook, but has no output, and it seems not to have registered the “text” component:

What am I doing wrong? I believe that I am missing something for the correct definition of the extended Bokeh model. Is there a way to obtain debug logging?

2 Likes

The Panel Custom class seems not to inherit from the right class. It should inherit from some Panel class. Don’t remember which.

FYI. With upcoming Panel 1.5 release we will make it much easier to make custom components. Merging all the learnings we have from Bokeh Models, Panels ReactiveHTML and AnyWidget. See Allow building ESM based components by philippjfr · Pull Request #5593 · holoviz/panel (github.com)

That’s awesome that you laid it out like this!

thank you for the hint. I researched a bit from other widgets, but I still cannot find a clear pattern. Here my latest changes:

  • changed “panel/panel/widgets/custom.py”:
import param

from ..models import Custom as _BkCustom

from typing import ClassVar, Mapping

from bokeh.model import Model
from bokeh.models.widgets import Div as _BkDiv

from .base import Widget

class Custom(Widget):

    avallo = param.String(default='this is the default')

    _rename: ClassVar[Mapping[str, str | None]] = {'name': None, 'avallo': 'text'}

    _widget_type: ClassVar[type[Model]] = _BkDiv
  • changed “panel/panel/models/custom.ts”:
import * as p from "@bokehjs/core/properties"
import {Div, DivView} from "@bokehjs/models/widgets/div"

export class CustomView extends DivView {
  declare model: Custom

}

export namespace Custom {
  export type Attrs = p.AttrsOf<Props>

  export type Props = Div.Props & {
    textolo: p.Property<string>
  }
}

export interface Custom extends Custom.Attrs {}

export class Custom extends Div {
  declare properties: Custom.Props
  declare __view_type__: CustomView

  constructor(attrs?: Partial<Custom.Attrs>) {
    super(attrs)
  }

  static {
    this.prototype.default_view = CustomView

    this.define<Custom.Props>(({Str}) => ({
      textolo:   [ Str, "Custom text" ],
    }))
  }
}```

now after a build I can successfully display an instance (pn.widgets.Custom(avallo=“ciao”)), but using default Bokeh models (i.e. _BkDiv). With my extended model (_BkCustom), which should really just be a wrapper around _BkDiv, I get the following exception:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File /opt/conda/lib/python3.11/site-packages/IPython/core/formatters.py:974, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    971     method = get_real_method(obj, self.print_method)
    973     if method is not None:
--> 974         return method(include=include, exclude=exclude)
    975     return None
    976 else:

File ~/package/panel/viewable.py:817, in Viewable._repr_mimebundle_(self, include, exclude)
    815 doc = Document()
    816 comm = state._comm_manager.get_server_comm()
--> 817 model = self._render_model(doc, comm)
    818 if config.embed:
    819     return render_model(model)

File ~/package/panel/viewable.py:735, in Viewable._render_model(self, doc, comm)
    733 if comm is None:
    734     comm = state._comm_manager.get_server_comm()
--> 735 model = self.get_root(doc, comm)
    737 if self._design and self._design.theme.bokeh_theme:
    738     doc.theme = self._design.theme.bokeh_theme

File ~/package/panel/viewable.py:666, in Renderable.get_root(self, doc, comm, preprocess)
    664 wrapper = self._design._wrapper(self)
    665 if wrapper is self:
--> 666     root = self._get_model(doc, comm=comm)
    667     if preprocess:
    668         self._preprocess(root)

File ~/package/panel/widgets/base.py:136, in Widget._get_model(self, doc, root, parent, comm)
    132 def _get_model(
    133     self, doc: Document, root: Optional[Model] = None,
    134     parent: Optional[Model] = None, comm: Optional[Comm] = None
    135 ) -> Model:
--> 136     model = self._widget_type(**self._get_properties(doc))
    137     root = root or model
    138     self._models[root.ref['id']] = (model, parent)

File /opt/conda/lib/python3.11/site-packages/bokeh/models/ui/ui_element.py:62, in UIElement.__init__(self, *args, **kwargs)
     61 def __init__(self, *args, **kwargs) -> None:
---> 62     super().__init__(*args, **kwargs)

File /opt/conda/lib/python3.11/site-packages/bokeh/model/model.py:119, in Model.__init__(self, *args, **kwargs)
    116 if "id" in kwargs:
    117     raise ValueError("initializing 'id' is not allowed")
--> 119 super().__init__(**kwargs)
    120 default_theme.apply_to_model(self)

File /opt/conda/lib/python3.11/site-packages/bokeh/core/has_props.py:306, in HasProps.__init__(self, **properties)
    304     if value is Undefined or value is Intrinsic:
    305         continue
--> 306     setattr(self, name, value)
    308 initialized = set(properties.keys())
    309 for name in self.properties(_with_props=True): # avoid set[] for deterministic behavior

File /opt/conda/lib/python3.11/site-packages/bokeh/core/has_props.py:344, in HasProps.__setattr__(self, name, value)
    341 if isinstance(descriptor, property): # Python property
    342     return super().__setattr__(name, value)
--> 344 self._raise_attribute_error_with_matches(name, properties)

File /opt/conda/lib/python3.11/site-packages/bokeh/core/has_props.py:379, in HasProps._raise_attribute_error_with_matches(self, name, properties)
    376 if not matches:
    377     matches, text = sorted(properties), "possible"
--> 379 raise AttributeError(f"unexpected attribute {name!r} to {self.__class__.__name__}, {text} attributes are {nice_join(matches)}")

AttributeError: unexpected attribute 'align' to Custom, possible attributes are context_menu, css_classes, css_variables, js_event_callbacks, js_property_callbacks, name, styles, stylesheets, subscribed_events, syncable, tags, text or visible

From what I can understand, the Constructor for the view does not cover the same attributes as the ones passed by the constructor from Widget (from “panel/panel/widgets/base.py”).
Does it rely on the use of UIElement? I also tried with the following classes:

from bokeh.models.widgets import InputWidget, Widget

but instead I do not get shown anything (no exception though).

If I understand correctly:

export class Custom extends Div

_widget_type: ClassVar[type[Model]] = _BkDiv

I think if you’re doing this

class Custom(Widget):

needs to be updated too.

Maybe Layoutable?

Or maybe the widget type, _BkDiv, is not correct. It should be Custom widget (part 4 below)

Here’s how I added ToggleIcon: Add toggle icon by ahuang11 · Pull Request #6034 · holoviz/panel · GitHub

Five parts:

The Panel side

  1. panel/widgets/icon.py

The TS parts
2. panel/models/icon.ts
3. panel/models/index.ts

Then the bokeh parts:
4. panel/models/init.py
5. panel/models/icon.py

It’s also important to note _rename for params that don’t propagate to the Typescript side and is only used in Python.