2025, Oct 03 19:00
How to Fix White Gaps and Extra Ticks in a Matplotlib Two-Color Contour Colorbar with BoundaryNorm
Learn why Matplotlib colorbars show white gaps or extra ticks in binary contours and fix it by setting BoundaryNorm with three edges: min, threshold, max.
When you want a binary contour visualization in Matplotlib — blue for values below a threshold and red for values above — the natural approach is to use a discrete colormap with two bins and add a colorbar for clarity. However, if the colorbar shows unexpected white sections or even an extra tick you didn’t ask for, the usual culprit is the way BoundaryNorm is configured.
Reproducing the issue
The following example builds an interpolated surface from scattered points, asks for a two-color contour with a threshold, and attaches a colorbar. The plot looks fine, but the colorbar ends up showing white and blue below the threshold, then white above it, and even adds a mysterious fourth tick.
from matplotlib import colors as mpl_colors, cm as mpl_cm
import numpy as nps
import matplotlib.pyplot as pl
import matplotlib.tri as mtri
nps.random.seed(19680801)
pt_count = 200
grid_x = 100
grid_y = 200
x_vals = nps.random.uniform(-2, 2, pt_count)
y_vals = nps.random.uniform(-2, 2, pt_count)
z_vals = x_vals * nps.exp(-x_vals**2 - y_vals**2)
x_lin = nps.linspace(-2.1, 2.1, grid_x)
y_lin = nps.linspace(-2.1, 2.1, grid_y)
tri_obj = mtri.Triangulation(x_vals, y_vals)
lin_interp = mtri.LinearTriInterpolator(tri_obj, z_vals)
Xg, Yg = nps.meshgrid(x_lin, y_lin)
z_grid = lin_interp(Xg, Yg)
fig_obj = pl.figure()
axis = fig_obj.subplots()
palette = ['blue', 'red']
cutoff = nps.mean(z_vals)
normer = mpl_colors.BoundaryNorm([cutoff - 1e-3, cutoff], ncolors=2, clip=True)
cmap_obj = mpl_colors.LinearSegmentedColormap.from_list('name', palette, N=2)
axis.contour(x_lin, y_lin, z_grid, norm=normer, cmap=cmap_obj)
scalar = mpl_cm.ScalarMappable(norm=normer, cmap=cmap_obj)
bar = fig_obj.colorbar(scalar, ax=axis)
bar.set_ticks([nps.min(z_grid), cutoff, nps.max(z_grid)])
bar.set_ticklabels([nps.min(z_grid), cutoff, nps.max(z_grid)])
What actually goes wrong
BoundaryNorm partitions your data range into bins defined by explicit edges. If you want exactly two colors, you need two bins, and that means three boundary values. Supplying only the values around the threshold produces the wrong binning. The result is a colorbar that doesn’t reflect a clean split at the threshold and may show unexpected gaps and a stray tick.
The fix
Define the bin edges to span the full data range, with the threshold in the middle. That gives you two bins: [lowest value, threshold] and [threshold, highest value].
from matplotlib import colors as mpl_colors, cm as mpl_cm
import numpy as nps
import matplotlib.pyplot as pl
import matplotlib.tri as mtri
nps.random.seed(19680801)
pt_count = 200
grid_x = 100
grid_y = 200
x_vals = nps.random.uniform(-2, 2, pt_count)
y_vals = nps.random.uniform(-2, 2, pt_count)
z_vals = x_vals * nps.exp(-x_vals**2 - y_vals**2)
x_lin = nps.linspace(-2.1, 2.1, grid_x)
y_lin = nps.linspace(-2.1, 2.1, grid_y)
tri_obj = mtri.Triangulation(x_vals, y_vals)
lin_interp = mtri.LinearTriInterpolator(tri_obj, z_vals)
Xg, Yg = nps.meshgrid(x_lin, y_lin)
z_grid = lin_interp(Xg, Yg)
fig_obj = pl.figure()
axis = fig_obj.subplots()
palette = ['blue', 'red']
cutoff = nps.mean(z_vals)
normer = mpl_colors.BoundaryNorm([nps.min(z_vals), cutoff, nps.max(z_vals)], ncolors=2, clip=True)
cmap_obj = mpl_colors.LinearSegmentedColormap.from_list('name', palette, N=2)
axis.contour(x_lin, y_lin, z_grid, norm=normer, cmap=cmap_obj)
scalar = mpl_cm.ScalarMappable(norm=normer, cmap=cmap_obj)
bar = fig_obj.colorbar(scalar, ax=axis)
bar.set_ticks([nps.min(z_grid), cutoff, nps.max(z_grid)])
bar.set_ticklabels([nps.min(z_grid), cutoff, nps.max(z_grid)])
This sets the two bin boundaries to cover [lowest value, mean value] and [mean value, highest value], which aligns the colormap and the colorbar with the intended binary threshold logic.
Why this detail matters
Binary thresholds are a common way to surface anomalies, flags, or decision boundaries. If the colorbar doesn’t match the plot semantics, you risk misreading the threshold or conveying inconsistent state to downstream users. Getting the bin edges right keeps the visualization faithful to the underlying rule.
Wrap-up
When using a two-color contour scheme split by a threshold, always provide BoundaryNorm with three boundaries that span the full data range with the threshold as the middle edge. This ensures the plot and the colorbar are consistent and makes the intent of the visualization immediately clear.
The article is based on a question from StackOverflow by Auguste Eclancher and an answer by Matt Pitkin.