Fixed top bar for widget?

Hello,
I have a column of plots that are all updated from a slider widget. It would be really handy if the slider was positioned in a fixed top bar so it can be adjusted when viewing plots at the bottom of the page. I think this could be handled by some CSS code e.g. https://www.w3schools.com/howto/howto_css_fixed_menu.asp
I’ve seen CSS can’t be used with Bokeh but another option might be using some sort of template. Is this the only way to replicate this type of styling?
Thanks.

I’ve seen CSS can’t be used with Bokeh

That’s not true, it absolutely can be used. But not to its fullest probably, because Bokeh has an internal layout mechanism.
With that being said, it should absolutely be possible to achieve what you want with CSS.

1 Like

Ah ok, I must have misunterstood what I read (I have no prior experience with html, JS, CSS !)
I’ll give it a go although looking through the source code of my html page is still a bit confusing.

Using the style editor in my browser I can add the following to .bk-root .bk-input-group;

position:fixed;
z-index:1;
width:340px;
margin-left: 25px; 

this fixes the slider to the top, however the bokeh tools e.g. scroll zoom, no longer work. I haven’t figured a way have whatever is under the cursor to be the focus of a pointer event regardless of z-index.

Is there a way to set this css style from my python script?

Yes, you can embed the CSS in an HTML template that you can either use as a separate file with bokeh serve or as a string when passing it to save.

ok, I’ve tried pass the updates to the css style using bokeh.resources.Resources but so far with no noticeable change to the page. I thought the code below should work but I may have misunderstood the documentation.

topbar = """
.bk-root .bk-input-group {
    position:fixed;
    z-index:1;
    width:365px;
    height:50px;
    margin-left: 25px; 
}
"""
res = Resources() 
res.css_raw.append(topbar) 
save(layout, resources=res)

I have tried another approach using Div

topbar = Div(text = """
<style>
.bk-root .bk-input-group {
      position:fixed;
      z-index:1;
      width:365px;
      height:50px;
      margin-left: 25px; }
</style> """)


save(layout, css_classes = ['topbar'])

but this is also doesn’t work.

Are there any examples showing how to add CSS as strings when saving?
Thanks.

As I have mentioned above, you have to use the template argument and provide it with an HTML template that already includes your CSS.

Ah ok, I misunderstood, I thought there were two methods.

  1. Provide a template file for the page. including extra CSS
  2. Let bokeh build an html page with some provided CSS snippets

but actually would I still need to supply a template for the whole page like the file.html on this page https://docs.bokeh.org/en/latest/docs/reference/core/templates.html ?

Yes, but you can just copy and modify the default one at https://github.com/bokeh/bokeh/blob/branch-2.2/bokeh/core/_templates/file.html

1 Like

Ah, I see! (I think)
I have very little html, JS, CSS, Jinja2…etc. experience. I thought the template would need much more detail.

I think I have now managed what I was hoping for.
In case anyone else would like implement something similar, this is a simple example:

import numpy as np
from bokeh.layouts import column, row, gridplot
from bokeh.models import CustomJS, Slider, Div
from bokeh.plotting import ColumnDataSource, figure, output_file, show, save
from bokeh.util.browser import view

output_file("slider_top_bar.html", 
            title="slider_top_bar.py example")

template = """
{% from macros import embed %}

<!DOCTYPE html>
<html lang="en">
  {% block head %}
  <head> 
    {% block inner_head %}
      <meta charset="utf-8">
      <title>{% block title %}{{ title | e if title else "Bokeh Plot" }}{% endblock %}</title>
      {% block preamble %}{% endblock %}
      {% block resources %}
        {% block css_resources %}
          {{ bokeh_css | indent(8) if bokeh_css }}
        {% block js_resources %}
          {{ bokeh_js | indent(8) if bokeh_js }}
        {% endblock %}
      {% endblock %}
      {% block postamble %}
      <style>
        .bk-root .bk-input-group {
              position:fixed;
              z-index:1;
              width:365px;
              height:50px;
              margin-left: 25px; }
        .bk-root .bk-toolbar {
              position:fixed;
              z-index:1;
              width:250px;
              height:30px;
              margin-left: 50px;
        }
      </style> 
      {% endblock %}
      {% endblock %}
    {% endblock %}
  </head>
  {% endblock %}
  {% block body %}
  <body>
    {% block inner_body %}
      {% block contents %}
        {% for doc in docs %}
          {{ embed(doc) if doc.elementid }}
          {% for root in doc.roots %}
            {% block root scoped %}
              {{ embed(root) | indent(10) }}
            {% endblock %}
          {% endfor %}
        {% endfor %}
      {% endblock %}
      {{ plot_script | indent(8) }}
    {% endblock %}
  </body>
  {% endblock %}
</html>

"""

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

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

tools = "pan,box_zoom,wheel_zoom,reset,hover"
plot = figure(y_range=(-10, 10), plot_width=400, plot_height=400, tools=tools)

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")

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

amp_slider.js_on_change('value', callback)
spacer = Div(width=400, height=40)
layout = gridplot([[amp_slider], [spacer], [plot]])  #, toolbar_location=None
save(layout, template=template)
view("/slider_top_bar.html")