Contour plots#

Contour plots are used to calculate and render lines of constant value in two-dimensional quadrilateral grids. Both the lines and the filled regions between lines can be rendered with a single function call.

Simple example#

Here is a simple example rendering both contour lines and filled polygon regions.

import numpy as np

from bokeh.palettes import Sunset8
from bokeh.plotting import figure, show

# Data to contour is the sum of two Gaussian functions.
x, y = np.meshgrid(np.linspace(0, 3, 40), np.linspace(0, 2, 30))
z = 1.3*np.exp(-2.5*((x-1.3)**2 + (y-0.8)**2)) - 1.2*np.exp(-2*((x-1.8)**2 + (y-1.3)**2))

p = figure(width=550, height=300, x_range=(0, 3), y_range=(0, 2))

levels = np.linspace(-1, 1, 9)
contour_renderer = p.contour(x, y, z, levels, fill_color=Sunset8, line_color="black")

colorbar = contour_renderer.construct_color_bar()
p.add_layout(colorbar, "right")

show(p)

By convention, z is the 2D array to contour and is defined on an x, y grid. Here the grid is a regularly-spaced Cartesian grid. levels contains the sequence of levels to contour.

Both line_color and fill_color are optional keyword arguments to contour(). line_color must be specified to draw contour lines and fill_color to draw filled contour polygons. They can be scalar or vector visual properties.

Note

The length of vector visual properties for contour lines is len(levels) and for filled contour polygons it is len(levels)-1.

In this example line_color is a scalar so every contour line is rendered as a solid black line. The fill_color is a vector so that the filled regions between contour levels are rendered with different colors.

There is a colorbar on the right of the plot which is obtained using construct_color_bar(). It automatically displays the same fill and line visual properties as the contour plot.

Polar grid example#

Here is a more complicated example showing other features available for contour plots.

import numpy as np

from bokeh.palettes import Cividis
from bokeh.plotting import figure, show

# Data to contour is a 2D sin wave on a polar grid.
radius, angle = np.meshgrid(np.linspace(0, 1, 20), np.linspace(0, 2*np.pi, 120))
x = radius*np.cos(angle)
y = radius*np.sin(angle)
z = 1 + np.sin(3*angle)*np.sin(np.pi*radius)

p = figure(width=550, height=400)

levels = np.linspace(0, 2, 11)

contour_renderer = p.contour(
    x=x, y=y, z=z, levels=levels,
    fill_color=Cividis,
    hatch_pattern=["x"]*5 + [" "]*5,
    hatch_color="white",
    hatch_alpha=0.5,
    line_color=["white"]*5 + ["black"] + ["red"]*5,
    line_dash=["solid"]*6 + ["dashed"]*5,
    line_width=[1]*6 + [2]*5,
)

colorbar = contour_renderer.construct_color_bar(title="Colorbar title")
p.add_layout(colorbar, "right")

show(p)

The grid is polar, wrapping around on itself, and there are many more visual properties including line, fill and hatch properties. Many of these are vector properties so that it is possible to emphasize, for example, positive and negative contour levels differently.

All visual properties can be scalar or vector of the correct length. Color visual properties line_color, fill_color and hatch_color support a few extra options for how they can be specified:

  • A sequence of colors that is longer or shorter than required will be resampled using interp_palette(). The length 256 palettes such as Cividis256 are useful here.

  • A palette collection such as Cividis may be used, which is a dictionary that maps from palette length (number of colors) to palette. If the collection contains a palette of the correct length then that is used. If the required length is outside of those available in the collection then the palette with the nearest length is used, linearly interpolated.

construct_color_bar() accepts other keyword arguments that are passed to the ContourColorBar constructor to set properties such as the title shown here.

Animated contours#

Bokeh can generate animated contour plots using bokeh serve as the contour calculations occur in Python. Here is an example taken from examples/app/contour_animated.py:

import numpy as np

from bokeh.driving import count
from bokeh.palettes import PiYG
from bokeh.plotting import curdoc, figure
from bokeh.plotting.contour import contour_data

x, y = np.meshgrid(np.linspace(-1, 1, 41), np.linspace(-1, 1, 41))
levels = np.linspace(-1.0, 1.0, 11)

def get_z(timestep):
    delta = 0.08*np.cos(timestep*0.15)
    amps  = [0.95, 0.95, -0.95, -0.95]
    xmids = [-0.4,  0.4, -0.4,  0.4]
    ymids = [-0.4,  0.4,  0.4, -0.4]
    rads  = [0.4 + delta, 0.4 + delta, 0.4 - delta, 0.4 - delta]

    z = np.zeros_like(x)
    for amp, xmid, ymid, rad in zip(amps, xmids, ymids, rads):
        z += amp*np.exp( -((x-xmid)**2 + (y-ymid)**2)/rad**2 )
    return z

@count()
def callback(timestep):
    z = get_z(timestep)
    new_contour_data = contour_data(x, y, z, levels)
    contour_renderer.set_data(new_contour_data)

fig = figure(width=600, height=450)

contour_renderer = fig.contour(x, y, get_z(0), levels, fill_color=PiYG,
                               line_color="black", line_width=[1]*5 + [3] + [1]*5)

colorbar = contour_renderer.construct_color_bar()
fig.add_layout(colorbar, 'right')

curdoc().add_periodic_callback(callback, 40)
curdoc().add_root(fig)

To run this on a Bokeh server use

bokeh serve --show contour_animated.py

The key sequence of actions to perform the animation are:

  1. Call contour() as usual, and store the returned ContourRenderer.

  2. Determine the updated z array, which might be read from file or calculated, for example.

  3. Pass the updated z and unchanged x, y and levels to contour_data() to generate a contour data object.

  4. Call set_data() with the new contour data object.

  5. Repeat from stage 2.

The animation example here assumes the grid, contour levels and visual properties are not changed. It is possible to do so, but care is needed to correctly deal with changing plot bounds and assignment of visual properties to contour levels, so it is usually easier to remove the old unwanted contour plot and replace it with a new one in these circumstances.

Advanced details#

The only compulsory keyword arguments to contour() are z, levels and at least one of fill_color and line_color. x and y are optional and if not specified a Cartesian grid will be used with a grid spacing of 1 in both directions.

To exclude grid points from the contour calculation then either use a NumPy masked array for z with the excluded grid points masked out, or set the z values of those grid points to np.nan.

Contour lines are implemented using a MultiLine glyph and filled contour polygons as a MultiPolygons glyph with the line_width set to zero.

The calculation of contours is performed by ContourPy. For information about this see the ContourPy documentation.

Note

Contouring was added to Bokeh version 3.0 and improvements are planned for future releases.