Exercise 4.5: Heat maps with Bokeh


A heat map is a type of plot where magnitude is expressed in terms of color. You can see an example of a heatmap generated using Bokeh here.

a) Write a function with call signature heatmap(x, y, z) (you can add any kwargs you like) that make a heat map from x, y, z data. For simplicity for this function, assume x and y are categorical variables.

b) 96 well plates are often used in analyzing biochemical reactions. Some absorbance data from a 96 well plate experiment are in the file ~git/bootcamp/data/96_well.csv. Use your heatmap() function to make a display of the data.

Solution


[1]:
import numpy as np
import pandas as pd

import bokeh.io
import bokeh.models
import bokeh.palettes
import bokeh.plotting

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

a) In our heatmap() function, we will allow for tooltips when hovering, inclusion of a colorbar, and specification of palette. Importantly, we make sure the x- and y- values are categorical. We then use rect glyphs to color the heat map.

[2]:
def heatmap(
    x,
    y,
    z,
    x_range=None,
    y_range=None,
    palette=bokeh.palettes.Viridis256,
    z_range=(None, None),
    x_label="x",
    y_label="y",
    z_label="z",
    flip_x_axis=False,
    flip_y_axis=False,
    colorbar=True,
    **kwargs
):
    """Create a heatmap for x, y, z data where the x and y data are categorial.

    Parameters
    ----------
    x : array_like
        x-values for heat map. Assumed to be categorical. Any entries are
        converted to strings.
    y : array_like
        y-values for heat map. Assumed to be categorical. Any entries are
        converted to strings.
    z : array_like
        z-values for heat map. These data are quantitative and displayed
        with color.
    x_range : array_like
        Array of unique values that determines the values of the x-axis.
    y_range : array_like
        Array of unique values that determines the values of the y-axis.
    palette : List of hex colors, default bokeh.palettes.Viridis256
        Color palette to use to make linear color mapper for heat map.
    z_range : 2-tuple, default (None, None)
        Range of allowed z-values. If an entry is None, the min or max
        is used.
    x_label : str, defualt "z"
        Label to be used in tool tips for the x-values.
    y_label : str, defualt "z"
        Label to be used in tool tips for the y-values.
    z_label : str, defualt "z"
        Label to be used in tool tips for the z-values.
    flip_x_axis : bool, default False
        If True, x-axis is reversed.
    flip_y_axis : bool, default False
        If True, y-axis is reversed.
    colorbar : bool, default True
        If True, display color bar.
    kwargs : dict
        All other kwargs are passed to bokeh.plotting.figure() when
        setting up the plot.

    Returns
    -------
    output : Bokeh plotting object
        Heatmap plot.
    """
    # Convert x and y values to strings; assuming evenly spread
    x_str = [str(x_val) for x_val in x]
    y_str = [str(y_val) for y_val in y]

    # Ranges of z-values
    z_min = z.min() if z_range[0] is None else z_range[0]
    z_max = z.max() if z_range[1] is None else z_range[1]

    # Categorical axis values
    if x_range is None:
        x_range = [str(x_val) for x_val in sorted(np.unique(x))]
    if y_range is None:
        y_range = [str(y_val) for y_val in sorted(np.unique(y))]
    if flip_x_axis:
        x_range = x_range[::-1]
    if flip_y_axis:
        y_range = y_range[::-1]

    # Set up defaults
    x_axis_label = kwargs.pop("x_axis_label", x_label)
    y_axis_label = kwargs.pop("y_axis_label", y_label)
    tools = kwargs.pop("tools", "pan,box_zoom,wheel_zoom,reset,hover,save")
    tooltips = kwargs.pop(
        "tooltips", [(x_label, "@x"), (y_label, "@y"), (z_label, "@z")]
    )
    toolbar_location = kwargs.pop("toolbar_location", "above")
    frame_height = kwargs.pop("frame_height", None)
    frame_width = kwargs.pop("frame_width", None)

    # Adjust frame heights and widths to have square rectangles
    if frame_width is not None:
        if frame_height is None:
            frame_height = frame_width * len(y_range) // len(x_range)
    else:
        if frame_height is None:
            frame_height = 250
        frame_width = frame_height * len(x_range) // len(y_range)

    # Data source
    source = bokeh.models.ColumnDataSource(
        dict(x_str=x_str, y_str=y_str, x=x, y=y, z=z)
    )

    # Color mapper
    mapper = bokeh.models.LinearColorMapper(palette=palette, low=z_min, high=z_max)

    # Figure
    p = bokeh.plotting.figure(
        x_range=x_range,
        y_range=y_range,
        frame_width=frame_width,
        frame_height=frame_height,
        x_axis_label=x_axis_label,
        y_axis_label=y_axis_label,
        tools=tools,
        tooltips=tooltips,
        toolbar_location=toolbar_location,
        **kwargs
    )

    p.rect(
        x="x_str",
        y="y_str",
        width=frame_width / frame_height * len(y_range) / len(x_range),
        height=frame_height / frame_width * len(x_range) / len(y_range),
        source=source,
        fill_color={"field": "z", "transform": mapper},
        line_color=None,
    )

    # Add color bar
    color_bar = bokeh.models.ColorBar(
        color_mapper=mapper, major_label_text_font_size="8px", border_line_color=None,
    )
    p.add_layout(color_bar, "right")

    return p

b) Let’s put it to use!

[3]:
df = pd.read_csv('data/96_well.csv')

p = heatmap(
    df["column"],
    df["row"],
    df["absorbance"],
    x_range=[str(x) for x in range(1, 13)],
    y_range=[a for a in reversed("ABCDEFGH")],
    x_label='column',
    y_label='row',
    z_label='absorbance',
    x_axis_label=None,
    y_axis_label=None,
)

bokeh.io.show(p)

Very nice!

Computing environment

[4]:
%load_ext watermark
%watermark -v -p numpy,pandas,bokeh,jupyterlab
Python implementation: CPython
Python version       : 3.9.12
IPython version      : 8.3.0

numpy     : 1.21.5
pandas    : 1.4.2
bokeh     : 2.4.2
jupyterlab: 3.3.2