Server does not render indicator gauge in certain scenarios

This issue was recently posted to Holoviz Panel discourse as Holoviz Discourse Topic 2310. I am re-posting in the bokeh discourse forum on the off-chance that something at a low-level related to the bokeh server is at play here.

VERSIONS: panel 0.11.3 and bokeh 2.3.2

BACKGROUND: A fairly complex app with numerous plots, interactive controls, etc. takes tens of seconds to up to a minute to initially render in certain configurations. Because of limitations /timeout parameters that are not configurable in the online service used to host the app, a periodic callback mechanism is implemented in the panel/bokeh server to incrementally build up the user interface.

ISSUE: If a panel indicator gauge (pn.indicators.Gauge) is included in this UI, the app fails to add the gauge and errors are observed in the JavaScript console.

The following is a highly simplified, minimal reproducible example to illustrate the behavior. With this minimal example, the UI fails for the first session that connects to the server. However, if the server is left up-and-running and the client browser is reloaded by the user, subsequent refreshes do generate the gauge.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""
from bokeh import __version__ as bokeh_version
from bokeh.models import Div
from bokeh.io import curdoc

import panel as pn


doc = curdoc()

_render_ll = []
_render_ll += [Div(text="bokeh {:}  panel {:}".format(bokeh_version, pn.__version__))]
_render_ll += [Div(text="PROGRESS WIDGET")]
_render_ll += [pn.widgets.Progress(value=50)]
_render_ll += [Div(text="SCORE GAUGE")]
_render_ll += [pn.indicators.Gauge(value=0.5, bounds=(0.0, 1.0), format='{value}')]

_layout = pn.Column()
_layout.server_doc(doc=doc)


def render_cb():
    """render callback
    """
    if _render_ll:
       print("INFO add item to layout {:}...".format(_render_ll[0]))
       _layout.append(_render_ll.pop(0))

cbobj = doc.add_periodic_callback(callback=render_cb, period_milliseconds=1000)

OUTPUT (INITIAL CLIENT SESSION)

OUTPUT (BROWSER RELOAD)

GitHub issue created on Holoviz panel GitHub. See Panel Issue 2330.

The discussion on the panel GitHub bug issue provided the following insight.

Oh I see, it’s because you’re adding it after initialization which means that the bundling code isn’t including the dependency on initialization. The easiest way to fix this is to load echarts explicitly at the top with:

pn.extension(‘echarts’)

Adding this fix does address the initialization errors seen in the JavaScript console of the client browser. However, the system still fails in new ways when building up the app via periodic callbacks.

Consider the following minimal example to illustrate. Here, if a model – in this case a Div – is added after the gauge, the gauge will briefly appear and then disappear, and its not possible to get it to show up again. The JavaScript console shows an error stack trace through bokeh–>panel–>echarts. (Screenshot follows.)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""
from bokeh import __version__ as bokeh_version
from bokeh.models import Div
from bokeh.models import Button
from bokeh.io import curdoc

import panel as pn

pn.extension('echarts')

doc = curdoc()

b = Button(button_type='primary', label='Show/Hide Gauge')

g = pn.indicators.Gauge(value=0.5, bounds=(0.0, 1.0), format='{value}', visible=True)

_render_ll = []
_render_ll += [Div(text="bokeh {:}  panel {:}".format(bokeh_version, pn.__version__))]
_render_ll += [b]
_render_ll += [pn.widgets.Progress(value=50)]
_render_ll += [Div(text="SCORE GAUGE")]
_render_ll += [g]
_render_ll += [Div(text="DONE ...")]

def show_hide_cb():
    _visible = g.visible
    print("show/hide gauge ... visible: {:}".format(_visible))
    g.visible = not(_visible)
    
def render_cb():
    """render callback
    """
    if _render_ll:
       print("INFO add item to layout {:}...".format(_render_ll[0]))
       _layout.append(_render_ll.pop(0))


_layout = pn.Column()
_layout.server_doc(doc=doc)

b.on_click(show_hide_cb)
cbobj = doc.add_periodic_callback(callback=render_cb, period_milliseconds=1000)

JavaScript Console SCREENSHOT

UPDATE in case anyone encounters this admittedly specialized use case and needs a workaround.

The following code avoids the removeChild error seen in the JavaScript console.

Fundamentally, what is being done here is creating a placeholder for the gauge via pn.Column() and then appending the gauge to that gauge-viewer at the end just prior to removing the periodic callback that builds up the UI.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""
from bokeh import __version__ as bokeh_version
from bokeh.models import Div
from bokeh.models import Button
from bokeh.io import curdoc

import panel as pn

pn.extension('echarts')

class Session:
    def __init__(self):
        self.doc = curdoc()

        self._b = None

        self._g = None
        self._g_view = None

        self._render_ll = []
        self._render_cbobj = None
    
        self._layout = None

    def go(self):
        self._b = Button(button_type='primary', label='Show/Hide Gauge')
        self._b.on_click(self._show_hide_cb)

        self._g = pn.indicators.Gauge(value=0.5, bounds=(0.0, 1.0), format='{value}', visible=True)
        self._g_view = pn.Column()

        self._render_ll = []
        self._render_ll += [Div(text="bokeh {:}  panel {:}".format(bokeh_version, pn.__version__))]
        self._render_ll += [self._b]
        self._render_ll += [pn.widgets.Progress(value=50)]
        self._render_ll += [Div(text="SCORE GAUGE")]
        self._render_ll += [self._g_view]
        self._render_ll += [Div(text="DONE ...")]

        self._layout = pn.Column()
        self._layout.server_doc(doc=self.doc)

        self._render_cbobj = self.doc.add_periodic_callback(callback=self._render_cb, period_milliseconds=1000)

    def _show_hide_cb(self):
        _visible = self._g.visible
        print("show/hide gauge ... visible: {:}".format(_visible))
        self._g.visible = not(_visible)
    
    def _render_cb(self):
        """render callback
        """
        print("PERIODIC CALLBACK ...")
        if self._render_ll:
            print("INFO add item to layout {:}...".format(self._render_ll[0]))
            self._layout.append(self._render_ll.pop(0))
        else:
            self._g_view.append(self._g)
            self.doc.remove_periodic_callback(self._render_cbobj)

S = Session()
S.go()