Skip to main content
v2026.11,858 entries · CC-BY 4.0

Contour Plot in Matplotlib: contour, contourf and clabel

How to draw a contour plot in Matplotlib: contour vs contourf, labeling with clabel, choosing levels, masked data and common errors.

Written and maintained by CASRAI Editorial Board

Last updated

A contour plot in Matplotlib draws lines of equal value across a two-dimensional grid: contour draws the lines and contourf fills the regions between them. Most disappointing results come from four things the basic call does not handle for you: which of the two functions to use, how to label the lines, what happens to missing data, and how the levels are chosen. This guide covers those first, then the steps. People also search for this as “matplotlib contour”; it is the same function.

What a contour plot is used for in research

A contour plot shows a quantity that depends on two variables, such as a fitted surface, a probability density over two parameters, a loss surface, or a measurement taken on a spatial grid. Lines of constant value make the shape of the surface readable where a scatter plot cannot. For a grid shown as colored cells instead, see heatmap in Matplotlib.

Install or access

Contour plotting is part of Matplotlib; no separate package is needed. The official installation page gives these commands:

python -m pip install -U pip
python -m pip install -U matplotlib

With conda, the same page lists conda install -c conda-forge matplotlib (conda-forge channel) and conda install matplotlib (main channel). If pip tries to build from source and fails, the page suggests adding --prefer-binary. The examples below also use NumPy; see import numpy as np and import matplotlib.pyplot as plt for the standard import conventions.

Step-by-step: draw a labeled contour plot

The steps follow the official Matplotlib contour demo.

  1. Build the grid. Create the coordinate vectors. X and Y may be either both 2D with the same shape as Z (the form np.meshgrid produces, used below) or both 1-D, with len(X) equal to the number of columns of Z and len(Y) equal to the number of rows; either way they must be monotonically ordered. Compute Z from them:
    import numpy as np
    import matplotlib.pyplot as plt
    
    x = np.arange(-3.0, 3.0, 0.025)
    y = np.arange(-2.0, 2.0, 0.025)
    X, Y = np.meshgrid(x, y)
    Z1 = np.exp(-X**2 - Y**2)
    Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
    Z = (Z1 - Z2) * 2
  2. Draw the lines. Call contour on an Axes with X, Y and Z, then label the lines with clabel:
    fig, ax = plt.subplots()
    CS = ax.contour(X, Y, Z)
    ax.clabel(CS, fontsize=10)
    plt.show()

    The signature is contour([X, Y,] Z, /, [levels], **kwargs), and the documentation states that X, Y and Z are positional-only arguments, so they cannot be passed by keyword.

  3. Choose levels yourself. Pass either a number of levels (the demo uses ax.contour(X, Y, Z, 6, colors='k')) or an explicit list. See the levels section below.
  4. Add a color scale if you fill the regions. Use contourf and fig.colorbar, as shown in the next section. For colorbar options see colorbar in Matplotlib.

contour vs contourf

Use contour for lines and contourf for filled bands. The documentation says that they “draw contour lines and filled contours, respectively” and that, except as noted, their function signatures and return values are the same, so you can switch between them by changing one word.

The one place the choice changes your code is labeling. clabel labels the lines of a contour set, so a filled plot with line labels needs a second, line-drawing call on top of the filled one. The official contourf demo does exactly this: it draws the filled plot, overlays lines on every second level, and attaches those lines to the colorbar:

fig, ax = plt.subplots(layout='constrained')
CS = ax.contourf(X, Y, Z, 10, cmap="bone")
CS2 = ax.contour(CS, levels=CS.levels[::2], colors='r')
cbar = fig.colorbar(CS)
cbar.add_lines(CS2)
plt.show()

Labels with clabel

ax.clabel(CS, fontsize=10) writes the level value on each line. Three options from the demo cover most needs:

  • Inline control. The inline argument controls whether the labels are drawn over the line segments, removing the lines beneath each label.
  • Manual placement. Labels can be placed manually by giving a list of positions in data coordinates: ax.clabel(CS, fontsize=10, manual=[(-1, -1.4), ...]).
  • Only some levels. Pass the levels to label and a format string, for example to label every second level:
    ax.clabel(CS, levels[1::2], fmt='%1.1f', fontsize=14)

Choosing levels

The official contourf demo warns that automatic selection of contour levels “is usually not such a good idea, because they don’t occur on nice boundaries.” For anything you will publish, pass explicit levels so the bands and the colorbar fall on round values. The demo gives an explicit list and per-band colors:

levels = [-1.5, -1, -0.5, 0, 0.5, 1]
CS3 = ax.contourf(X, Y, Z, levels,
                  colors=('r', 'g', 'b'),
                  extend='both')
CS3.cmap.set_under('yellow')
CS3.cmap.set_over('cyan')

The extend argument decides what happens to data outside the listed levels. The demo illustrates all four settings: 'neither', 'both', 'min' and 'max'. With extend='both', values below and above the range are drawn with the colormap’s under and over colors, which you set with set_under and set_over.

Masked and NaN data

Missing values need care. The documentation notes that contourf converts NaN values to masked, and that contouring simply excludes masked or NaN regions: it draws nothing at all in them rather than using the colormap’s “bad” color. Therefore cmap.set_bad("red") would have no effect. If you need missing regions to be visible, mark them another way, for example with a separate patch or annotation on the same Axes.

Output and how to read it

  • Close lines mean steep change. Lines are drawn at constant values of Z, so where they bunch together Z changes quickly, and where they are far apart it changes slowly.
  • Read the labels, not the colors. With labeled lines or a colorbar that has the lines added, you can read levels directly; with automatic levels the values may be awkward numbers, which is why explicit levels are recommended above.
  • Dashed lines are negative. In a single-color plot, negative contours default to dashed. To draw them solid instead, set the rc parameter:
    CS = ax.contour(X, Y, Z, 6, colors='k')   # negative contours default to dashed
    plt.rcParams['contour.negative_linestyle'] = 'solid'

For choosing a colormap that stays readable for color-vision differences, see designing effective scientific figures.

Troubleshooting

Symptom What to check
Error about the shapes of X, Y and Z The documentation says X and Y must both be 2D with the same shape as Z, or both 1-D with len(X) equal to the number of columns of Z and len(Y) equal to the number of rows of Z. They must also be monotonically ordered. np.meshgrid produces the 2D form.
TypeError when passing X= or Z= as keywords X, Y and Z are positional-only; pass them by position.
No labels on a filled plot clabel labels the lines of a contour set. Overlay a contour call and label that set.
Blank area where data is missing Contouring excludes masked and NaN regions and set_bad does not apply to them.
Dashed lines you did not ask for Negative contours in a single-color plot are dashed by default; set contour.negative_linestyle to 'solid'.
Bands not on round values Pass explicit levels instead of relying on automatic selection.

Free alternatives

Within Matplotlib itself, the options below are all free and part of the same library; the choice depends on whether you want lines, fills, or both.

Approach What it draws Source
contour Contour lines only pyplot.contour documentation
contourf Filled contours, same signature as contour pyplot.contour documentation
contourf plus contour overlay Filled bands with labeled lines on chosen levels, and lines added to the colorbar contourf demo

FAQ

What is the difference between contour and contourf in Matplotlib?

contour draws contour lines and contourf draws filled contours. Their signatures and return values are the same except where the documentation notes otherwise.

How do I label contour lines?

Call ax.clabel(CS) on the object returned by contour. Use fontsize, fmt, inline and manual to control appearance and position; for a filled plot, overlay a contour call and label that.

How do I set the contour levels?

Pass a number or an explicit list after Z, for example levels = [-1.5, -1, -0.5, 0, 0.5, 1]. The official demo advises against automatic levels because they do not fall on round boundaries.

Why are some contour lines dashed?

In a single-color plot, negative contours are dashed by default. Set plt.rcParams['contour.negative_linestyle'] = 'solid' to change this.

Sources

Follow CASRAI

Research-administration guidance, standards updates and independent tool reviews.

Ask CASRAI · free to try

Ask about Contour Plot in Matplotlib: contour, contourf and clabel

Ask your first 2 questions free below. Subscribers get 150 a day for $29 a month.

An AI assistant specialized in research administration. It cites the sources behind every answer, labels web answers and says when it can't answer.

Answers draw on CASRAI's guides and dictionary plus the federal and funder documents we index: Federal Register, Grants.gov, Regulations.gov and UKRI.

Works on this site and inside Claude, Cursor and the AI tools you already use.

Everything CASRAI publishes — this page, the dictionary, the guides and the news — stays free to read, with no account and no card.

Referenced across the research world

University of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logoUniversity of Cambridge logoColumbia University logoCrossref logoUniversity of Edinburgh logoHarvard University logoUniversity of Oxford logoPrinceton University logoStanford School of Medicine logoUniversity College London logoORCID logo
  • University of Cambridge logo
  • Columbia University logo
  • Crossref logo
  • University of Edinburgh logo
  • Harvard University logo
  • University of Oxford logo
  • Princeton University logo
  • Stanford School of Medicine logo
  • University College London logo
  • ORCID logo

View CASRAI adoption →

Ask CASRAI · Regulatory Radar

Research-admin question? Get an answer that links its sources.

An AI assistant specialized in research administration. Every answer links its sources to check before you act. 2 questions free, no account. $29/month after.

  • Answers draw on CASRAI's guides and dictionary plus the federal and funder documents we index: Federal Register, Grants.gov, Regulations.gov and UKRI.
  • Every answer numbers its sources and links each one, so you can check the source yourself.