Styling Bokeh plots


[2]:
import polars as pl

import colorcet

import bokeh.io
import bokeh.plotting
import bokeh.models
import bokeh.themes

bokeh.io.output_notebook()
Loading BokehJS ...

We have seen how to use Bokeh (and the higher-level plotting package iqplot) to make interactive plots. We have seen how to adjust plot size, axis labels, glyph color, etc. We have also seen how to style plots generated with iqplot. But we have just started to touch the surface of how we might customize plots. In this lesson, we investigate ways to stylize Bokeh plots to our visual preferences.

We will again make use of the face-matching data set. We’ll naturally start by loading the data set.

[3]:
fname = os.path.join(data_path, "gfmt_sleep.csv")
df = (
    pl.read_csv(fname, null_values="*")
    .with_columns((pl.col('sci') <= 16).alias('insomnia'))
)

Styling Bokeh plots as they are built

Bokeh figures and renderers (which are essentially the glyphs) have a plethora of attributes pertaining to visual appearance that may be adjusted at instantiation and after making a plot.

A color palette an ordering of colors that are used for glyphs, usually corresponding to categorical data. Colorcet’s Glasbey Category 10 provides a good palette for categorical data, and we store this as our categorical colors for plotting.

[4]:
cat_colors = colorcet.b_glasbey_category10

Now we can build the plot. Since the data are percentages, we will set the axes to go from zero to 100 and enforce that the figure is square. We will also include a title as well so we can style that.

[5]:
p = bokeh.plotting.figure(
    frame_width=300,
    frame_height=300,
    x_axis_label="confidence when correct",
    y_axis_label="condifence when incorrect",
    title="GMFT with sleep conditions",
    x_range=[0, 100],
    y_range=[0, 100],
)

In styling this plot, we will also put the legend outside of the plot area. This is a bit trickier than what we have been doing using the legend_label kwarg in p.scatter(). To get a legend outside of the plot area, we need to:

  1. Assign each glyph to a variable.

  2. Instantiate a bokeh.models.Legend object using the stored variables containing the glyphs. This is instantiated as bokeh.models.Legend(items=legend_items), where legend_items is a list of 2-tuples. In each 2-tuple, the first entry is a string with the text used to label the glyph. The second entry is a list of glyphs that have the label.

  3. Add the legend to the figure using the add_layout() method.

Now, we add the glyphs, storing them as variables normal_glyph and insom_glyph.

[6]:
normal_glyph = p.scatter(
    source=df.filter(~pl.col('insomnia')).to_dict(),
    x="confidence when correct",
    y="confidence when incorrect",
    color=cat_colors[0],
)

insom_glyph = p.scatter(
    source=df.filter(pl.col('insomnia')).to_dict(),
    x="confidence when correct",
    y="confidence when incorrect",
    color=cat_colors[1],
)

Now we can construct and add the legend.

[7]:
# Construct legend items
legend_items = [('normal', [normal_glyph]), ('insomnia', [insom_glyph])]

# Instantiate legend
legend = bokeh.models.Legend(items=legend_items, click_policy='hide')

# Add the legend to the right of the plot
p.add_layout(legend, 'right')

Now, let’s take a look at this beauty!

[8]:
bokeh.io.show(p)

Styling Bokeh plots after they are built

After building a plot, we sometimes want to adjust styling. To do so, we need to change attributes of the object p. For example, let’s look at the font of the x-axis label.

[9]:
p.xaxis.axis_label_text_font
[9]:
'helvetica'

We can also look at the style and size of the font.

[10]:
p.xaxis.axis_label_text_font_style, p.xaxis.axis_label_text_font_size
[10]:
('italic', '13px')

So, the default axis labels for Bokeh are italicized 13 pt Helvetica. I personally think this choice if fine, but we may have other preferences.

To find out all of the available options to tweak, I usually type something like p. and hit tab to see what the options are. Finding p.xaxis is an option, then type p.xaxis. and hit tab again to see the styling option there.

Using this technique, we can set some obnoxious styling for this plot. I will make all of the fonts non-italicized, large papyrus. I can also set the background and grid colors. Note that in Bokeh, any named CSS color or any valid HEX code, entered as a string, is a valid color.

Before we do the obnoxious styling, we will do one adjustment that is useful. Note in the above plot that the glyphs at the end of the plot are cropped. We would like the whole glyph to show. To do that, we set the level of the glyphs to be 'overlay'. To do that, we extract the first two elements of the list of renderers, which contains the glyphs, and set the level attribute.

[11]:
p.renderers[0].level = 'overlay'
p.renderers[1].level = 'overlay'

Now we can proceed to make our obnoxious styling.

[12]:
# Obnoxious fonts
p.xaxis.major_label_text_font = 'papyrus'
p.xaxis.major_label_text_font_size = '14pt'
p.xaxis.axis_label_text_font = 'papyrus'
p.xaxis.axis_label_text_font_style = 'normal'
p.xaxis.axis_label_text_font_size = '20pt'
p.yaxis.major_label_text_font = 'papyrus'
p.yaxis.major_label_text_font_size = '14pt'
p.yaxis.axis_label_text_font = 'papyrus'
p.yaxis.axis_label_text_font_style = 'normal'
p.yaxis.axis_label_text_font_size = '20pt'
p.title.text_font = 'papyrus'
p.title.text_font_size = '18pt'
p.legend.label_text_font = 'papyrus'

# Align the title center
p.title.align = 'center'

# Set background and grid color
p.background_fill_color = 'blanchedalmond'
p.legend.background_fill_color = 'chartreuse'
p.xgrid.grid_line_color = 'azure'
p.ygrid.grid_line_color = 'azure'

# Make the ticks point inward (I *hate* this!)
# Units are pixels that the ticks extend in and out of plot
p.xaxis.major_tick_out = 0
p.xaxis.major_tick_in = 10
p.xaxis.minor_tick_out = 0
p.xaxis.minor_tick_in = 5
p.yaxis.major_tick_out = 0
p.yaxis.major_tick_in = 10
p.yaxis.minor_tick_out = 0
p.yaxis.minor_tick_in = 5

bokeh.io.show(p)

This is truly hideous, but it demonstrates how we can go about styling plots after they are made.

Bokeh themes

Bokeh has several built-in themes which you can apply to all plots in a given document (e.g., in a notebook). Please see the documentation for details about the built-in themes. I personally prefer the default styling to all of their themes, but your opinion may differ.

You may also specify custom themes using JSON or YAML. As an example, we can specify a theme such that plots are styled like the default style of the excellent plotting packages Vega-Altair/Vega-Lite/Vega. If we use JSON formatting, we can specify a theme as a dictionary of dictionaries, as below.

[13]:
altair_theme_dict = {
    "attrs": {
        "Axis": {
            "axis_line_color": "dimgray",
            "minor_tick_out": 0,
            "major_tick_in": 0,
            "major_tick_line_color": "dimgray",
            "major_label_text_font_size": "7.5pt",
            "axis_label_text_font_size": "8pt",
            "axis_label_text_font_style": "bold",
        },
        "Scatter": {
            "fill_alpha": 0,
            "line_width": 2,
            "size": 5,
            "line_alpha": 0.7,
        },
        "ContinuousTicker": {
            "desired_num_ticks": 10
        },
        "figure": {
            "frame_width": 350,
            "frame_height": 300,
        },
        "Grid": {
            "grid_line_color": "lightgray",
            "level": "underlay",
        },
        "Legend": {
            "border_line_color": None,
            "background_fill_color": None,
            "label_text_font_size": "7.5pt",
            "title_text_font_size": "8pt",
            "title_text_font_style": "bold",
        },
        "Renderer": {
            "level": "overlay"
        },
        "Title": {
            "align": "center",
        },
    }
}

To activate the theme, we convert it to a Bokeh theme and then add it to the curdoc(), or the current document.

[14]:
altair_theme = bokeh.themes.Theme(json=altair_theme_dict)

bokeh.io.curdoc().theme = altair_theme

Now the theme is activated, and future plots will have this theme by default. Let’s remake our plot using this theme. For convenience later on, I will write a function to generate this scatter plot that we will use to test various styles.

[15]:
def gfmt_plot():
    """Make a plot for testing out styles in this notebook."""
    p = bokeh.plotting.figure(
        frame_width=300,
        frame_height=300,
        x_axis_label="confidence when correct",
        y_axis_label="condifence when incorrect",
        title="GMFT with sleep conditions",
        x_range=[0, 100],
        y_range=[0, 100],
    )

    normal_glyph = p.scatter(
        source=df.filter(~pl.col('insomnia')).to_dict(),
        x="confidence when correct",
        y="confidence when incorrect",
        color=cat_colors[0],
    )

    insom_glyph = p.scatter(
        source=df.filter(pl.col('insomnia')).to_dict(),
        x="confidence when correct",
        y="confidence when incorrect",
        color=cat_colors[1],
    )

    # Construct legend items
    legend_items = [('normal', [normal_glyph]), ('insomnia', [insom_glyph])]

    # Instantiate legend
    legend = bokeh.models.Legend(items=legend_items, click_policy='hide')

    # Add the legend to the right of the plot
    p.add_layout(legend, 'right')

    return p

bokeh.io.show(gfmt_plot())

We could also style our plots to resemble the default “dark” styling of Seaborn.

[16]:
seaborn_theme_dict = {
    "attrs": {
        "figure": {
            "background_fill_color": "#eaeaf2",
            "frame_height": 300,
            "frame_width": 350,
        },
        "Axis": {
            "axis_line_color": None,
            "minor_tick_out": 0,
            "major_tick_in": 0,
            "major_tick_out": 0,
            "major_label_text_font_size": "7.5pt",
            "axis_label_text_font_size": "7.5pt",
            "axis_label_text_font_style": "normal",
        },
        "Legend": {
            "border_line_color": "darkgray",
            "background_fill_color": "#eaeaf2",
            "border_line_width": 0.75,
            "label_text_font_size": "7.5pt",
        },
        "Grid": {
            "grid_line_color": "#FFFFFF",
            "grid_line_width": 0.75,
        },
        "Title": {
            "align": "center",
            'text_font_style': 'normal',
            'text_font_size': "8pt",
        },
    }
}

seaborn_theme = bokeh.themes.Theme(json=seaborn_theme_dict)
bokeh.io.curdoc().theme = seaborn_theme

Let’s make the plot, yet again, with this new styling.

[17]:
bokeh.io.show(gfmt_plot())

Finally, we can specify a style I like. Note that I do not specify that the glyphs are at an overlay level, since by default Bokeh will scale the axes such that the glyphs are fully contained in the plot area. I also put the toolbar above the plot, which is usually not a problem because I generally prefer not to title my plots, opting instead for good textual description in captions or in surrounding text.

[18]:
jb_theme_dict = {
    "attrs": {
        "Axis": {
            "axis_line_color": "dimgray",
            "major_tick_line_color": "dimgray",
            "major_label_text_font_size": "7.5pt",
            "axis_label_text_font_size": "9pt",
            "axis_label_text_font_style": "bold",
        },
        "Scatter": {
            "size": 5,
            "fill_alpha": 0.8,
            "line_width": 0,
        },
        "figure": {
            "frame_height": 300,
            "frame_width": 350,
            "toolbar_location": "above",
        },
        "Grid": {
            "grid_line_color": "lightgray",
            "level": "underlay",
        },
        "Legend": {
            "border_line_color": "darkgray",
            "border_line_width": 0.75,
            "background_fill_color": "#ffffff",
            "background_fill_alpha": 0.7,
            "label_text_font": "helvetica",
            "label_text_font_size": "7.5pt",
            "title_text_font": "helvetica",
            "title_text_font_size": "8pt",
            "title_text_font_style": "bold",
        },
        "Renderer": {
            "level": "overlay"
        },
        "Title": {
            "text_font": "helvetica",
            "text_font_size": "10pt",
            'text_font_style': 'bold',
        },
    }
}

jb_theme = bokeh.themes.Theme(json=jb_theme_dict)
bokeh.io.curdoc().theme = jb_theme

bokeh.io.show(gfmt_plot())

Finally, if I were to make this particular plot, I would do it without a title and with axes leaving a little buffer.

[19]:
p = bokeh.plotting.figure(
    frame_width=300,
    frame_height=300,
    x_axis_label="confidence when correct",
    y_axis_label="condifence when incorrect",
    x_range=[-2.5, 102.5],
    y_range=[-2.5, 102.5],
)

normal_glyph = p.scatter(
    source=df.filter(~pl.col('insomnia')).to_dict(),
    x="confidence when correct",
    y="confidence when incorrect",
    color=cat_colors[0],
)

insom_glyph = p.scatter(
    source=df.filter(pl.col('insomnia')).to_dict(),
    x="confidence when correct",
    y="confidence when incorrect",
    color=cat_colors[1],
)

# Construct legend items
legend_items = [('normal', [normal_glyph]), ('insomnia', [insom_glyph])]

# Instantiate legend
legend = bokeh.models.Legend(items=legend_items, click_policy='hide')

# Add the legend to the right of the plot
p.add_layout(legend, 'right')

bokeh.io.show(p)

You can play with these themes and develop your own style as you see fit. As you can see, Bokeh is highly configurable, and you can really make the plots your own!

Computing environment

[20]:
%load_ext watermark
%watermark -v -p numpy,polars,bokeh,jupyterlab
Python implementation: CPython
Python version       : 3.12.3
IPython version      : 8.25.0

numpy     : 1.26.4
polars    : 1.1.0
bokeh     : 3.4.1
jupyterlab: 4.0.13