Matplotlib — Pictures from Numbers

Type four or five lines, press Run, and a picture appears. Change a number, run it again, and the picture changes. That is the whole of this lesson, and matplotlib is the Python library that does it — line charts, scatters, bars, histograms, grids of colour, anything you can describe as numbers.

The first run fetches matplotlib, so that one needs a connection; every run after that on the same device starts straight away.

The shortest thing that draws something

Make a CodeBook (a notebook), set the cell's language to Python, and type this:

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import io, base64

plt.plot([1, 4, 9, 16])

buf = io.BytesIO()
plt.savefig(buf, format="png")
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()

Then add an Image cell directly underneath — the button between cells, then Image. Run the code cell. A chart appears in the image cell: four points, rising.

Only one of those lines is your picture. plt.plot([1, 4, 9, 16]) is the drawing; everything above and below it is the same every single time. So learn it once, keep it, and forget it.

Change the numbers and run it again:

plt.plot([16, 9, 4, 1])

Now it falls instead of rising. That loop — change a number, see the picture — is what the rest of this page is for.

Where a picture can appear

A picture has to be drawn somewhere, and that place is a cell or a node next to your code:

You're working inPut the code inPut the picture in
A CodeBook notebooka Code cellan Image cell directly below it
A workflow canvasa Code nodean Image node joined to it

In both cases the rule is the same: your code returns the picture, and the thing next to it draws it. That is what the last line of the frame does.

A code file opened on its own is the exception. Its Run button shows you what the code returned as text — useful for numbers, not for pictures. So do your plotting in a notebook or on a canvas, where there is something to draw into.

The frame, once

Keep this and paste your drawing into the middle:

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import io, base64

# ---- your picture goes here ----

buf = io.BytesIO()
plt.savefig(buf, format="png")
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()

Worth knowing what the two odd lines do, even though you never have to change them:

  • matplotlib.use("Agg") tells matplotlib to draw into a file rather than into a window. On a desktop computer matplotlib would normally open a window; here there is no window to open, so you say so up front. It has to come before import matplotlib.pyplot, which is why it sits on the first line.
  • The last three lines save the finished chart into memory and hand it back as the cell's value, which is what the image cell next door draws.

plt.show() is the line you will see in books and on the web. Here it does nothing — there is no window for it to open — and it prints a note telling you so. Use the frame instead of plt.show().

From here on, every example shows only the middle.

Say what it is — title, labels, legend

A chart nobody can read is decoration. Four lines fix that:

plt.plot([0, 1, 2, 3], [0, 1, 4, 9], label="squares")
plt.title("My first chart")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.grid(True)

Note that plt.plot took two lists this time: the across values, then the up values. With one list, matplotlib numbers the points 0, 1, 2, 3… for you — which is what happened in the first example.

label= names the line, and plt.legend() is what actually draws the little key. One without the other gets you nothing.

More than one line

Call plt.plot again. Everything you draw lands on the same chart until you say otherwise:

months = ["Jan", "Feb", "Mar", "Apr", "May"]
rain = [82, 61, 55, 48, 37]
sun  = [41, 58, 96, 141, 182]

plt.plot(months, rain, label="Rain (mm)")
plt.plot(months, sun, label="Sunshine (hrs)")
plt.legend()

The across values can be words, as here, and matplotlib spaces them evenly and labels them.

Two charts side by side

plt.subplots gives you a row, a column or a grid of charts at once:

fig, (left, right) = plt.subplots(1, 2, figsize=(8, 3))

left.plot([1, 2, 3], [1, 4, 9])
left.set_title("Squares")

right.plot([1, 2, 3], [1, 8, 27])
right.set_title("Cubes")

fig.tight_layout()

plt.subplots(1, 2) means one row, two columns. Each chart is its own object with its own methods — and note the names change slightly: plt.title(...) becomes left.set_title(...), plt.xlabel(...) becomes left.set_xlabel(...). fig.tight_layout() stops the labels colliding.

For a 2×2 grid, unpack two rows:

fig, ((a, b), (c, d)) = plt.subplots(2, 2, figsize=(7, 5))

Scatter — points, not lines

When you have pairs of measurements and no line to join them:

height = [1.60, 1.65, 1.70, 1.75, 1.80, 1.85]
weight = [55, 62, 68, 73, 80, 88]

plt.scatter(height, weight)
plt.xlabel("Height (m)")
plt.ylabel("Weight (kg)")

Bars

fruit = ["Apples", "Pears", "Plums", "Figs"]
sold  = [312, 189, 97, 45]

plt.bar(fruit, sold, color="seagreen")
plt.ylabel("Sold")

plt.barh does the same thing lying on its side, which is kinder to long labels.

Histograms — the shape of a pile of numbers

A histogram takes one long list and counts how many fall into each band. This is where NumPy starts earning its place, because it can make you five hundred numbers in one line:

import numpy as np

heights = np.random.default_rng(0).normal(170, 8, 500)

plt.hist(heights, bins=20, color="steelblue", edgecolor="white")
plt.xlabel("Height (cm)")

bins=20 is the number of bands. Turn it down to 5 and the shape goes blocky; turn it up to 100 and it goes spiky. Changing that one number and re-running is the fastest way to understand what a histogram actually is.

A smooth curve

Two hundred points close together read as a curve. NumPy's linspace gives you evenly spaced values between two ends:

import numpy as np

x = np.linspace(0, 10, 200)
plt.plot(x, np.sin(x))

Styling

Every drawing command takes the same handful of extras:

plt.figure(figsize=(6, 4))
plt.plot([1, 2, 3, 4], [2, 5, 3, 8],
         color="crimson", linestyle="--", marker="o", linewidth=2)
plt.ylim(0, 10)
WhatDoesTry
color=the colour"crimson", "seagreen", "#ff8800", "tab:blue"
linestyle=solid, dashed, dotted"-", "--", ":", "-."
marker=a mark at each point"o", "s", "^", "x", "."
linewidth=how thick1, 2, 4
alpha=how see-through0.3 to 1.0
figsize=(w, h)how big the whole chart is(6, 4), (10, 3)
plt.xlim / plt.ylimcrop the viewplt.ylim(0, 10)

For a whole new look in one line, use a ready-made style before you draw:

plt.style.use("ggplot")
plt.plot([1, 4, 9])

"ggplot", "fivethirtyeight", "bmh", "grayscale", "dark_background" and the "seaborn-" family are all there. print(plt.style.available) lists every one.

Marking a point

plt.plot([1, 2, 3], [2, 8, 4])
plt.annotate("the peak", xy=(2, 8), xytext=(2.1, 6),
             arrowprops=dict(arrowstyle="->"))

xy is what you're pointing at; xytext is where the words sit.

A grid of colour

Give imshow a grid of numbers and it colours them in — heatmaps, images, anything laid out in rows and columns:

import numpy as np

grid = np.add.outer(np.arange(20), np.arange(20)) % 7

plt.imshow(grid, cmap="viridis")
plt.colorbar()

cmap picks the colour scheme: "viridis", "plasma", "inferno", "coolwarm", "gray".

One chart at a time

There is one thing that will confuse you exactly once. matplotlib keeps drawing onto the same chart until told otherwise — and in a notebook, that carries on between runs. Draw one line, run it, draw one line, run it, and the second picture has two lines in it.

So when you want a clean sheet, say so at the top of your drawing:

plt.close("all")
plt.plot([3, 2, 1])

plt.figure() does the same job — it starts a new chart and leaves the old one alone. Either is fine; plt.close("all") is the one to reach for when a picture has something in it you don't remember asking for.

Making it bigger or sharper

The saving line takes a couple of useful extras:

buf = io.BytesIO()
plt.savefig(buf, format="png", dpi=120, bbox_inches="tight")

dpi is sharpness — 120 or 150 for something you want to look at closely. bbox_inches="tight" trims the white space around the edge, which is the fix when a long label is getting clipped.

Working with NumPy

matplotlib will happily take plain Python lists, and every example above except the last three does. But once you want a curve, a random sample, or a calculation applied to a thousand values at once, you want NumPy next to it — that is the pairing matplotlib was built for. Anything NumPy produces can go straight into plt.plot, plt.hist or plt.imshow with no conversion.

Try this now

Five minutes, one notebook, and you will have met most of the page:

  1. Make a CodeBook, set the cell to Python, and paste the first example. Add an Image cell underneath. Run it.
  2. Change [1, 4, 9, 16] to [1, 4, 9, 16, 25, 36]. Run it again.
  3. Add plt.title("Squares") above the saving lines. Run it.
  4. Add a second plt.plot(...) with different numbers. Two lines now.
  5. Put plt.close("all") at the top and run twice, then take it out and run twice. That's the one surprise, met on purpose.
  6. Swap plt.plot for plt.bar, then plt.scatter. Same numbers, three different pictures.
  • NumPy — the arrays and the maths that feed these charts
  • SymPy and SciPy — algebra, and the heavier numerical work
  • CodeBook Tour — every control in the notebook, cell by cell
  • Code Node — the same Python on a workflow canvas
  • Languages, and Where They Run — what runs on the device in your hand, and what needs a computer