How do I use jsPanel with Panel and ReactiveHTML?

Hi All

I would like to have a floating panel in Panel. For example for having a floating panel of widgets on top of a map.

I’ve seen @nghenzi2019 experiment with this. But I’ve also found jsPanel which provides a lot of possibilities.

Starting Point

As a starting point I can create something like

import panel as pn
import param

RAW_CSS = """
body {
    margin: 0px;}
"""

if not RAW_CSS in pn.config.raw_css:
    pn.config.raw_css.append(RAW_CSS)
pn.extension()


class JsPanel(pn.reactive.ReactiveHTML):
    _template = """
<div id="floating-panel"></div>
"""

    __css__ = ["https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.css"]

    __javascript__ = [
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js",
    ]

    _scripts = {
        "render": """
var config = {
    position: 'right-top',
    content: '<p>I want my widgets here!</p>',
    contentSize: '500 350',
    headerTitle: 'Settings',
    theme: 'primary',
    callback: function(panel) {
        // do something if needed ...
    }
}
jsPanel.create(config);
"""
    }


pn.Column(
    pn.Spacer(background="lightblue", sizing_mode="stretch_both", margin=0),
    JsPanel(),
    sizing_mode="stretch_both",
).servable()

Adding the widget to the app

I can add a widget to the app like shown below

import panel as pn
import param

RAW_CSS = """
body {
    margin: 0px;}
"""

if not RAW_CSS in pn.config.raw_css:
    pn.config.raw_css.append(RAW_CSS)
pn.extension()


class JsPanel(pn.reactive.ReactiveHTML):
    object = param.Parameter()

    _template = """
<div id="element">${object}</div>
"""
    __css__ = ["https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.css"]

    __javascript__ = [
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js",
    ]

    _scripts = {
        "render": """
var config = {
    position: 'right-top',
    content: '<p>I want my widget here!</p>',
    contentSize: '500 350',
    headerTitle: 'Settings',
    theme: 'primary',
    callback: function(panel) {
        // do something if needed ...
    }
}
jsPanel.create(config);
"""
    }

pn.Column(
    JsPanel(object=pn.widgets.Button(name="My Widget")),
    pn.Spacer(sizing_mode="stretch_both", margin=0),
    sizing_mode="stretch_both",background="lightblue"
).servable()

I can use a div as content of the Panel

If I set

_template = """
<div id="element">Some Text I've added to a div element that is used inside the jsPanel</div>
"""

and content to element, the div is shown inside the Panel.

import panel as pn
import param

RAW_CSS = """
body {
    margin: 0px;}
"""

if not RAW_CSS in pn.config.raw_css:
    pn.config.raw_css.append(RAW_CSS)
pn.extension()


class JsPanel(pn.reactive.ReactiveHTML):
    _template = """
<div id="element">Some Text I've added to a div element that is used inside the jsPanel</div>
"""
    __css__ = ["https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.css"]

    __javascript__ = [
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js",
    ]

    _scripts = {
        "render": """
var config = {
    position: 'right-top',
    content: element,
    contentSize: '500 350',
    headerTitle: 'Settings',
    theme: 'primary',
    callback: function(panel) {
        // do something if needed ...
    }
}
jsPanel.create(config);
"""
    }

pn.Column(
    JsPanel(object=pn.widgets.Button(name="My Widget")),
    pn.Spacer(sizing_mode="stretch_both", margin=0),
    sizing_mode="stretch_both",background="lightblue"
).servable()

Move the widget inside the div element

If I set

_template = """
<div id="element">${object}</div>
"""

where object=pn.widgets.Button(name="My Widget") then I get the error

'element' is not defined

import panel as pn
import param

RAW_CSS = """
body {
    margin: 0px;}
"""

if not RAW_CSS in pn.config.raw_css:
    pn.config.raw_css.append(RAW_CSS)
pn.extension()


class JsPanel(pn.reactive.ReactiveHTML):
    object = param.Parameter()

    _template = """
<div id="element">${object}</div>
"""
    __css__ = ["https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.css"]

    __javascript__ = [
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js",
    ]

    _scripts = {
        "render": """
var config = {
    position: 'right-top',
    content: element,
    contentSize: '500 350',
    headerTitle: 'Settings',
    theme: 'primary',
    callback: function(panel) {
        // do something if needed ...
    }
}
jsPanel.create(config);
"""
    }

pn.Column(
    JsPanel(object=pn.widgets.Button(name="My Widget")),
    pn.Spacer(sizing_mode="stretch_both", margin=0),
    sizing_mode="stretch_both",background="lightblue"
).servable()
1 Like

Amazing. This is much better of what I’ve done. I was only using a div with css position relative. I will check it and add some comments here !

1 Like

I’ve added a bug report here ReactiveHTML: Element not found · Issue #2742 · holoviz/panel (github.com) and reported a similar problem here ReactiveHTML: TypeError: Object of type Button is not JSON serializable · Issue #2743 · holoviz/panel (github.com).

if you subclass listlike like in the flexbox model it works

import panel as pn
import param
from panel.layout.base import ListLike

RAW_CSS = """
body {
    margin: 0px;}
"""

if not RAW_CSS in pn.config.raw_css:
    pn.config.raw_css.append(RAW_CSS)
pn.extension()


class JsPanel(ListLike, pn.reactive.ReactiveHTML):
    _template = """
<div id="element">Some Text I've added to a d

 {% for obj in objects %}
  <div id="flex-item-{{ loop.index0 }}">
    ${objects[{{ loop.index0 }}]}
  </div>
  {% endfor %}

iv element that is used inside the jsPanel</div>
"""
    __css__ = ["https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.css"]

    __javascript__ = [
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js",
    ]

    _scripts = {
        "render": """
var config = {
    position: 'right-top',
    content: element,
    contentSize: '500 350',
    headerTitle: 'Settings',
    theme: 'primary',
    callback: function(panel) {
        // do something if needed ...
    }
}
jsPanel.create(config);
"""
    }

    def __init__(self, *objects, **params):
        super().__init__(objects=list(objects), **params)

from bokeh.plotting import figure

p = figure(sizing_mode='stretch_both')
p.circle([1,2,3],[4,5,6])
pn.Column(
    JsPanel(pn.Column(pn.widgets.Button(name="My Widget"), p)),
    pn.Spacer(sizing_mode="stretch_both", margin=0),
    sizing_mode="stretch_both",background="lightblue"
).servable()
2 Likes

Awesome @nghenzi2019 .

My conclusion is that 1) we can make this work with a list 2) we need some working examples of layouts where we want to place a single widget instead of a list of widgets. Right now its seems buggy.

To finalize this I will create a small app using your solution and share it here :slight_smile:

1 Like

Ok. So here is something nice to get started :slight_smile:

floating-panel-speedup

import panel as pn
from panel import config
import param
from panel.layout.base import ListLike

RAW_CSS = """
body {
    margin: 0px;}
"""

if not RAW_CSS in pn.config.raw_css:
    pn.config.raw_css.append(RAW_CSS)
pn.extension(sizing_mode="stretch_width")

POSITIONS = [
    'center',
    'left-top',
    'center-top',
    'right-top',
    'right-center',
    'right-bottom',
    'center-bottom',
    'left-bottom',
    'left-center',
]


class JsPanel(ListLike, pn.reactive.ReactiveHTML):
    """A list-like floating panel for Panel :-)"""
    position = param.Selector(default='right-top', objects=POSITIONS)
    offsetx = param.Integer(0)
    offsety = param.Integer(0)
    theme = param.String(default="primary")

    config = param.Dict({}, doc="Additional configuration. C.f. the jsPanel docs. Takes precedence over parameter values")

    _template = """
<div id="element" class="bk-root" style="padding:8px;padding-right:30px">
{% for obj in objects %}
<div id="flex-item-{{ loop.index0 }}">
    ${objects[{{ loop.index0 }}]}
</div>
{% endfor %}
</div>
"""

    _scripts = {
        "render": """
console.log(data.config)
var config = {
    headerTitle: data.name,
    content: element,
    theme: data.theme,
    position: {
        at: data.position,
        my: data.position,
        offsetX: data.offsetx,
        offsetY: data.offsety,
    },
    contentSize: `${model.width} ${model.height}`,

}
config = {...config, ...data.config}
console.log(config)
jsPanel.create(config);
"""
}

    __css__ = ["https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.css"]

    __javascript__ = [
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js",
        "https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js",
    ]

    def __init__(self, *objects, **params):
        super().__init__(objects=list(objects), **params)

from bokeh.plotting import figure

import holoviews as hv
import panel as pn
import panel.widgets as pnw

from bokeh.sampledata.autompg import autompg

df = autompg.copy()

ORIGINS = ['North America', 'Europe', 'Asia']

# data cleanup
df.origin = [ORIGINS[x-1] for x in df.origin]

df['mfr'] = [x.split()[0] for x in df.name]
df.loc[df.mfr=='chevy', 'mfr'] = 'chevrolet'
df.loc[df.mfr=='chevroelt', 'mfr'] = 'chevrolet'
df.loc[df.mfr=='maxda', 'mfr'] = 'mazda'
df.loc[df.mfr=='mercedes-benz', 'mfr'] = 'mercedes'
df.loc[df.mfr=='toyouta', 'mfr'] = 'toyota'
df.loc[df.mfr=='vokswagen', 'mfr'] = 'volkswagen'
df.loc[df.mfr=='vw', 'mfr'] = 'volkswagen'
del df['name']

columns = sorted(df.columns)
discrete = [x for x in columns if df[x].dtype == object]
continuous = [x for x in columns if x not in discrete]
quantileable = [x for x in continuous if len(df[x].unique()) > 20]

x = pnw.Select(name='X-Axis', value='mpg', options=quantileable)
y = pnw.Select(name='Y-Axis', value='hp', options=quantileable)
size = pnw.Select(name='Size', value='accel', options=['None'] + quantileable)
color = pnw.Select(name='Color', value='accel', options=['None'] + quantileable)

@pn.depends(x.param.value, y.param.value, color.param.value, size.param.value)
def create_figure(x, y, color, size):
    opts = dict(cmap='rainbow', responsive=True, min_width=1800, min_height=800, line_color='black')
    if color != 'None':
        opts['color'] = color
    if size != 'None':
        opts['size'] = hv.dim(size).norm()*20
    return hv.Points(df, [x, y], label="%s vs %s" % (x.title(), y.title())).opts(**opts)

widgets = JsPanel(x, y, color, size, name="Settings", theme="crimson", offsety=150, offsetx=-100, config={"headerControls": {"close": 'remove',},})
header = pn.pane.Markdown("   Awesome Panel - Floating Panel :-)", background="rgb(219, 20, 60)", margin=0, style={"color": "white", "padding-left": "25px", "font-weight": "bold", "font-size": "25px"})

pn.Column(header, pn.panel(create_figure, sizing_mode="stretch_both"), widgets).servable()

Thanks @nghenzi2019 for helping me make this possible.

2 Likes

I’ve created a feature request for a FloatingPanel

Panel needs a Floating Panel :slight_smile: · Issue #2745 · holoviz/panel (github.com)

Please check it out and add your input to the design discussion. Thanks.