Geoviews/Bokeh slider disappers in Streamlit

I’m trying to use a Holoviews/Bokeh dynamic map which will give me a Bokeh based slider in Streamlit.

This code comes from the top example here. When the equivalent is run in jupyter, I get a Bokeh slider that allows me to modify the graph. However, in Streamlit the slider disappears and I see the static plot.

Can anyone tell me how to get the slider back?

import streamlit as st
import numpy as np
import holoviews as hv

frequencies = [0.5, 0.75, 1.0, 1.25]

def sine_curve(phase, freq):
    xvals = [0.1* i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase+freq*x) for x in xvals]))

dmap = hv.DynamicMap(sine_curve, kdims=['phase', 'frequency'])
st.write(hv.render(dmap.redim.range(phase=(0.5,1), frequency=(0.5,1.25))))

Welcome to the Streamlit community @tdhopper!

It’s probably the same, but what happens if you call st.bokeh_chart instead of st.write as the last line?

It’s the same :crying_cat_face:

If you switch the backend, it becomes animated:

import streamlit as st
import numpy as np
import holoviews as hv

frequencies = [0.5, 0.75, 1.0, 1.25]


def sine_curve(phase, freq):
    xvals = [0.1 * i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]))


dmap = hv.DynamicMap(sine_curve, kdims=["phase", "frequency"])
a = hv.render(dmap.redim.range(phase=(0.5, 1), frequency=(0.5, 1.25)), backend="bokeh")


st.bokeh_chart(a)

I’ll have to dig in a little more to find out where the sliders are generated from.

Yeah, sorry, I was using the bokeh backend in my version

1 Like

In this example where the sliders are added explicitly they work fine

That example opens a standalone HTML file in a different window. It’s probably likely that we could intercept that HTML string and use the components.html window to display it, but that feels clumsy.

Is there functionality that the bokeh sliders provides that a regular set of Streamlit sliders doesn’t?

@randyzwitch It seems to be much faster because it doesn’t have to redraw the entire widget

What I meant is that I copied that example into a streamlit window and did st.bokeh_chart(layout) instead of the last two lines

import numpy as np

from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider
from bokeh.plotting import ColumnDataSource, figure, output_file, show

x = np.linspace(0, 10, 500)
y = np.sin(x)

source = ColumnDataSource(data=dict(x=x, y=y))

plot = figure(y_range=(-10, 10), plot_width=400, plot_height=400)

plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

amp_slider = Slider(start=0.1, end=10, value=1, step=.1, title="Amplitude")
freq_slider = Slider(start=0.1, end=10, value=1, step=.1, title="Frequency")
phase_slider = Slider(start=0, end=6.4, value=0, step=.1, title="Phase")
offset_slider = Slider(start=-5, end=5, value=0, step=.1, title="Offset")

callback = CustomJS(args=dict(source=source, amp=amp_slider, freq=freq_slider, phase=phase_slider, offset=offset_slider),
                    code="""
    const data = source.data;
    const A = amp.value;
    const k = freq.value;
    const phi = phase.value;
    const B = offset.value;
    const x = data['x']
    const y = data['y']
    for (var i = 0; i < x.length; i++) {
        y[i] = B + A*Math.sin(k*x[i]+phi);
    }
    source.change.emit();
""")

amp_slider.js_on_change('value', callback)
freq_slider.js_on_change('value', callback)
phase_slider.js_on_change('value', callback)
offset_slider.js_on_change('value', callback)

layout = row(
    plot,
    column(amp_slider, freq_slider, phase_slider, offset_slider),
)

import streamlit as st

st.bokeh_chart(layout)
1 Like

I think I’ll have to get back to you on this. Now that we see that a Layout object will get rendered (as your code snippet shows), I’ll have to try to figure out what DynamicMap is actually doing.

Cross posted on the pyviz forums to see if they can help

1 Like

Hi @tdhopper

I am not sure you will be able to use a dynamic map.

I think you cannot convert a dynamic map to a bokeh model. A dynamic map needs a live kernel and a different architecture/ communication channels than what Streamlit provides. You will need something like Jupyter, Panel or Voila for that. Maybe Dash as they recently added support for HoloViews. See Working with Plot and Renderers — HoloViews 1.14.1 documentation

What you can do is to use a Holomap. It generates all the plots up front. You still cannot convert that to a bokeh model. But according to the link above you can convert this to html. Then you can embed that html in Streamlit.

The below works

import streamlit as st
import streamlit.components.v1 as components
import numpy as np
import holoviews as hv

frequencies = [0.5, 0.75, 1.0, 1.25]


def sine_curve(phase, freq):
    xvals = [0.1 * i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]))

def get_plots():
    plots = {}
    for i in range(10):
        for j in range(10):
            phase = 0.5+float(i)*0.05
            frequency = 0.5+float(j)+0.075
            plots[(phase, frequency)] = sine_curve(phase, frequency)
    return plots

@st.cache
def get_html():
    plots = get_plots()
    hmap = hv.HoloMap(plots, kdims=["phase", "frequency"])
    renderer=hv.renderer('bokeh')
    html=renderer.static_html(hmap)
    return html

html = get_html()
components.html(html, height=600)

All HoloViz objects including HoloViews and Panel can be saved as static reports in HTML.

You can read more about how you can use Panels .get_root to get the underlying bokeh model or export to html and keep some degree of interactivity here Deploy and Export — Panel 0.10.3 documentation

There is a lengthy discussion on the pros and cons here Tips for saving interactive plots as html - HoloViews - HoloViz Discourse

If you use Panel and .get_root you get the plot and widgets. But you don’t get the interactivity because Streamlit does not provide any communication channels it knows how to use.

import streamlit as st
import numpy as np
import holoviews as hv
import panel as pn

frequencies = [0.5, 0.75, 1.0, 1.25]

def sine_curve(phase, freq):
    xvals = [0.1 * i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]))

dmap = hv.DynamicMap(sine_curve, kdims=["phase", "frequency"])
dmap = dmap.redim.range(phase=(0.5, 1), frequency=(0.5, 1.25))
plot = pn.panel(dmap)

st.bokeh_chart(plot.get_root())

If you use .save like below you can save a dynamic map to html. But you cannot display it in Streamlit due to some CORS issue.

import streamlit as st
import streamlit.components.v1 as components
import numpy as np
import holoviews as hv
import panel as pn
import pathlib

frequencies = [0.5, 0.75, 1.0, 1.25]

def sine_curve(phase, freq):
    xvals = [0.1 * i for i in range(100)]
    return hv.Curve((xvals, [np.sin(phase + freq * x) for x in xvals]))

dmap = hv.DynamicMap(sine_curve, kdims=["phase", "frequency"])
dmap = dmap.redim.range(phase=(0.5, 1), frequency=(0.5, 1.25))
plot = pn.panel(dmap)

path = pathlib.Path(__file__).parent/'test.html'
plot.save(path, embed=True, max_states=100)
html=path.read_text()
components.html(html, height=600)

1 Like

Has there been any change in this behaviour?

I am trying to use a dynamic map controlled by a Param/Panel setup. I can render the sliders and the bokeh plot/dynamic map in Streamlit but they do not interact.

The app is a frontend for an XArray dataset object and the dynamic map is a way of making the visualisation work faster online. So it’s not an option to use a HoloMap or an HTML copy.