2

I'm creating a dashboard in PyViz panel, which uses bokeh under the hood as far as I understand. It is easy to use and I quickly get to a 90% solution, but the final layout is easier (for me) to just do with plain old CSS (grid layout or something). I can attach html ids and classes to each widget/plot/column/row etc from Python, but I have not found a way to stop bokeh from including a long string of inline css on each div it produces. I want the layout to be responsive, no hard coded width/height or such things.

I am already serving the panel code from main.ipynb in a directory with a template/index.html, so a solution involving templates is perfectly OK. I do not want to create the whole dashboard in many different roots that are added to the template if I don't need to, the generated html from rows and cols is perfectly fine for my purposes if I could just remove the generated inline css attributes.

I can write a Python function that traverses the panel/bokeh widget tree and modifies the widgets if that is what it takes. I can of course also traverse the DOM in Javascript and remove the style attributes from the divs as they are created, but that seems less elegant than hacking together some !important css.

Aside: does anyone know how to stop bokeh from changing the html title attribute from Javascript, or how to set the title from panel?

An example dashboard:

This is a minimalistic dashboard without any custom styling. It produces hard-coded width and height on each div (as seen from Firefox devtools element inspector). The dashboard is left aligned and does not efficiently use the screen real estate (lots of white space to the left, the histogram does not span the same horizontal width as the row above). The layout is also not responsive when the page is resized.

import numpy
import pandas
import param
import panel
import holoviews
from IPython.display import display


panel.extension()
holoviews.extension('bokeh')

# Create syntetic dataset
N= 1000
age = numpy.random.gamma(10, 1.5, N)
defects = numpy.clip(numpy.random.chisquare(5, N) * age - 10, 0, 1e100).astype(int)
df = pandas.DataFrame({'age': age, 'defects': defects})
display(df.head(3))

# default sizing_mode
SM = 'stretch_width'


class DashboardDefinition(param.Parameterized):
    age = param.Range((0, 20), (0, None), softbounds=(0, 30))
    histogram = param.ObjectSelector('defects', ['defects', 'age'])

    def filter_data(self, table):
        return table.select(age=self.age)

    @param.depends('age')
    def make_scatter(self):
        scatter = holoviews.Points(df, ['age', 'defects'])
        selected = self.filter_data(scatter)
        return panel.panel(selected, sizing_mode=SM)

    @param.depends('age', 'histogram')
    def make_histogram(self):
        table = holoviews.Table(df, self.histogram)
        table = self.filter_data(table)
        frequencies, edges = numpy.histogram(table[self.histogram], 15)
        hist = holoviews.Histogram((frequencies, edges))
        return panel.panel(hist, sizing_mode=SM)

    def layout(self):
        return panel.Column(panel.Row(self.param,
                                      self.make_scatter,
                                      sizing_mode=SM),
                            self.make_histogram,
                            sizing_mode=SM)


dashboard = DashboardDefinition(name='Dashboard parameters')
dash_panel = dashboard.layout() 

# Define the dashboard
template = """
{% extends base %}

{% block title %}Hello world{% endblock %}

{% block contents %}
<h1>My dashboard</h1>
<p>This is my currently unstyled dashboard</p>
<br>
{{ embed(roots.dash1) }}
{% endblock %}
"""
tmpl = panel.Template(template)
tmpl.add_panel('dash1', dash_panel)
tmpl.servable()

# Uncomment to preview dashboard in jupyter notebook
#dash_panel
0

I got around my concrete problem by setting responsive=True in holoviews (thanks to xavArtley in https://gitter.im/pyviz/pyviz for helping me with this part). From the bokeh docs it seems like this option is removed, but it is there in holoviews, and using it makes the inline css match what I want.

You must style the elements before the javascript runs, otherwise the sizes will be wrong, but you can do a lot by putting custom css in the template preamble (postamble will not work, at least not in Firefox).

A working example

Here the container around the dashboard decides the dashboard component sizes, and not the inline css (which is set by Javascript to match the surrounding div element):

import numpy
import pandas
import param
import panel
import holoviews


panel.extension()
holoviews.extension('bokeh')

# Create syntetic dataset
N= 1000
age = numpy.random.gamma(10, 1.5, N)
defects = numpy.clip(numpy.random.chisquare(5, N) * age - 10, 0, 1e100).astype(int)
df = pandas.DataFrame({'age': age, 'defects': defects})


class DashboardDefinition(param.Parameterized):
    age = param.Range((0, 20), (0, None), softbounds=(0, 30))
    histogram = param.ObjectSelector('defects', ['defects', 'age'])

    def filter_data(self, table):
        return table.select(age=self.age)

    @param.depends('age')
    def make_scatter(self):
        scatter = holoviews.Points(df, ['age', 'defects'])
        selected = self.filter_data(scatter)
        hv_obj = selected.options(responsive=True, aspect=3)
        return panel.panel(hv_obj)

    @param.depends('age', 'histogram')
    def make_histogram(self):
        table = holoviews.Table(df, self.histogram)
        table = self.filter_data(table)
        frequencies, edges = numpy.histogram(table[self.histogram], 25)
        hist = holoviews.Histogram((frequencies, edges))
        hv_obj = hist.options(responsive=True, aspect=4)
        return panel.panel(hv_obj)

    def layout(self):
        return panel.Column(panel.Row(self.param,
                                      self.make_scatter,
                                      sizing_mode='stretch_width'),
                            self.make_histogram,
                            sizing_mode='stretch_width')


dashboard = DashboardDefinition(name='Dashboard parameters')
dash_panel = dashboard.layout() 

# Define the dashboard
template = """
{% extends base %}

{% block preamble %}
<style>
div#dashboard {
    width: 80%;
    margin: 0 auto;
}
</style>
{% endblock preamble %}

{% block contents %}
<div id="dashboard">
  <h1>My dashboard</h1>
  {{ embed(roots.dash1) }}
</div>
{% endblock contents %}
"""
tmpl = panel.Template(template)
tmpl.add_panel('dash1', dash_panel)
tmpl.servable(title="My page title")

# Uncomment to preview dashboard in jupyter notebook
#dash_panel

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.