Section 1 of 10

1 Before you start

Before you begin, take a few minutes to check what you already know and how confident you feel. You will see the same questions again at the end of the module — this helps both you and us measure what you have learned. Click an option for every question and confidence rating, then click Next to continue.

Pre-test

You add label="Cohort 1" to plt.plot but the legend does not appear in your figure. What else do you need?

Pre-test

You want to draw a horizontal reference line at y=7.0 across the whole plot to mark a clinical threshold. Which call adds it?

Pre-test

Your x-axis shows the numbers 0, 1, 2, but you want the ticks to read "Baseline", "Week 1", "Week 2" instead. Which call does this?

Pre-test

You write fig, axes = plt.subplots(1, 2) and try axes[0, 0].plot(...). Python raises IndexError: too many indices for array. What is the fix?

Pre-test

Inside a figure made with fig, axes = plt.subplots(1, 2), you want to give the LEFT panel its own title. Which approach is correct?

Pre-test

In a multi-panel figure, the subplot titles and axis labels overlap and collide with each other. Which single call fixes the spacing automatically?

Pre-test

You call plt.savefig("figure.png") after plt.show() in a script and the saved PNG file is blank. What should you do?

Pre-test

You are exporting a figure for a journal submission that must stay perfectly sharp at any zoom level. Which file format is the best choice?

Pre-confidence

I can build a one-panel matplotlib figure with axis labels, a title, and a legend, and save it as a PNG.

Not at all confident
Fully confident
Pre-confidence

I can build a two- or four-panel figure with plt.subplots, label every panel, and save the result as a 300-dpi PDF ready to drop into a paper or slide.

Not at all confident
Fully confident
Pre-confidence

I can customize a plot with a threshold line, custom axis limits, and named tick labels, and style its lines with colours, markers, and line styles.

Not at all confident
Fully confident
Section 2 of 10

2 Introduction

In Part I you set up figures and drew the core plot types: line, scatter, bar, and histogram. A plot that is correct, though, is not yet a plot that is clear. It still needs labels and a legend a reader can follow, it often needs to sit alongside related panels, and it usually needs to be saved to a file you can drop into a report or slide.

This second part is about turning those plots into finished, presentation-ready figures:

  • Customizing plots — labels, titles, colors, line styles, markers, and the legend that ties it all together.
  • Subplots and multiple figures — putting two, four, or more panels in the same figure with plt.subplots.
  • Saving figures — writing your finished plot to a PNG or PDF you can drop into a slide, an email, or a paper.

Try every snippet in the Python Scratchpad on the right. By the end of this part you will be able to label and style a figure, lay out a multi-panel figure with plt.subplots, and save a publication-ready image to disk.

Section 3 of 10

3 Essential matplotlib customizations

A plot without context is just a line. Adding labels, titles, and legends makes the difference between a figure that confuses and one that convinces.

Here is a practical guide to formatting, structuring, and exporting your plots so they are always ready for reports or presentations.

Section 3.1 of 10

3.1 Labels, titles and legends

Get into the habit of adding these foundational elements to every figure, even quick throwaway ones:

  • plt.xlabel() and plt.ylabel() – Always include the units in parentheses (e.g., "Weight (kg)").
  • plt.title() – Sits at the top of the figure to summarize the data.
  • plt.grid(True) – Adds a faint background grid to make reading values easier.
  • plt.legend() – Creates the legend box. Crucial detail: You must pass label="Your Name" inside your plt.plot() calls first, otherwise the legend will be completely empty.
Building a clear matplotlib figure by adding labels, title, grid, and legend one step at a time.
Building a clear matplotlib figure by adding labels, title, grid, and legend one step at a time.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

months = [0, 3, 6, 12]
weight_kg = [82, 79, 75, 73]

plt.plot(months, weight_kg)
plt.xlabel("Months since baseline")
plt.ylabel("Weight (kg)")
plt.title("Patient weight during 12-month follow-up")
plt.grid(True)
plt.show()

To customize the look of your lines, use the color (names like steelblue or hex codes), linestyle (-, --, :), and marker (o, s, ^) arguments.

Cycling through color, linestyle, and marker arguments in plt.plot to show how each one changes a line's appearance.
Cycling through color, linestyle, and marker arguments in plt.plot to show how each one changes a line's appearance.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

months = [0, 3, 6, 12]
patient_a = [82, 79, 75, 73]
patient_b = [78, 76, 75, 74]

plt.plot(months, patient_a, color="steelblue", linestyle="-", marker="o", label="Patient A")
plt.plot(months, patient_b, color="darkorange", linestyle="--", marker="s", label="Patient B")
plt.xlabel("Months since baseline")
plt.ylabel("Weight (kg)")
plt.title("Two patients, twelve months")
plt.legend()
plt.show()
Section 3.2 of 10

3.2 Threshold, limits and custom ticks

Often, you need to highlight a specific clinical cutoff or zoom in on a region of interest.

  • Threshold lines – Use plt.axhline(y=...) to draw a horizontal line across the entire plot, or plt.axvline(x=...) for a vertical one. Both accept color and linestyle arguments.
  • Axis limits – Override the default zoom using plt.xlim(lo, hi) and plt.ylim(lo, hi).
  • Custom ticks – plt.xticks(positions, labels) allows you to replace standard numbers with named categories (e.g., mapping [0, 1] to ["Baseline", "Week 12"]).
Marking thresholds with axhline and axvline, overriding the default zoom with xlim and ylim, and replacing numeric ticks with named categories using xticks.
Marking thresholds with axhline and axvline, overriding the default zoom with xlim and ylim, and replacing numeric ticks with named categories using xticks.

Try this snippet to see how plt.axhline() draws a horizontal threshold line across a plot

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

months = [0, 3, 6, 12]
hba1c = [7.2, 8.1, 6.5, 5.9]
plt.plot(months, hba1c, marker="o")
plt.axhline(y=7.0, color="red", linestyle="--", label="Threshold")
plt.xlabel("Month")
plt.ylabel("HbA1c (%)")
plt.legend()
plt.show()

Try this snippet to see how to customize the x- and y-axis limits and ticks.

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

x = [0, 1, 2, 3, 4]
y = [10, 25, 30, 35, 40]
plt.plot(x, y, marker="o")
plt.xlim(-0.5, 4.5)
plt.ylim(0, 50)
plt.xticks([0, 1, 2, 3, 4], ["Baseline", "Week 1", "Week 2", "Week 4", "Week 12"])
plt.show()
Section 4 of 10

4 Subplots and multiple figures

When you need to compare datasets, placing them in a single figure is almost always cleaner than generating separate images. The reader's eye can compare panels directly without flipping pages.

fig, axes = plt.subplots(nrows, ncols) creates a grid. It returns the overall figure object and an array of individual axes.

Comparing datasets in one matplotlib figure with plt.subplots(1, 3), which returns a fig container and an axes array of panels.
Comparing datasets in one matplotlib figure with plt.subplots(1, 3), which returns a fig container and an axes array of panels.

When working with subplots, you must switch to object-oriented syntax:

  • Use ax instead of plt – Inside a multi-panel figure, standard plt.something calls only apply to the most recently created axis, which gets confusing fast. Instead, call methods directly on the specific axis object you want to change: ax.plot(), ax.set_title(), and ax.set_xlabel().
  • figsize=(width, height) – Sets the overall figure size in inches. Matplotlib's default is quite small. For slides or papers, bump it up to 8 by 5 or 10 by 6. Match your aspect ratio to your grid—use a wide figure for side-by-side panels, and a tall one for stacked panels.
  • plt.tight_layout() – Always call this once at the very end. It automatically adjusts the spacing between subplots so that titles and axis labels do not collide and overlap.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

ages = [54, 61, 47, 72, 38, 65, 49, 58, 71, 44]
bmi  = [27.3, 24.1, 31.2, 22.8, 29.5, 26.0, 30.1, 25.4, 23.7, 28.2]

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

axes[0].hist(ages, bins=8)
axes[0].set_title("Age distribution")
axes[0].set_xlabel("Age (years)")

axes[1].hist(bmi, bins=5)
axes[1].set_title("BMI distribution")
axes[1].set_xlabel("BMI")

plt.tight_layout()
plt.show()
Building a side-by-side matplotlib figure: set the canvas size with figsize, draw on each panel using its own axis object, then call tight_layout to fix the spacing.
Building a side-by-side matplotlib figure: set the canvas size with figsize, draw on each panel using its own axis object, then call tight_layout to fix the spacing.

When you create a 2D grid (like 2 by 2), axes becomes a 2D array. You index it using axes[row, col]:

Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 2, figsize=(8, 6))

axes[0, 0].plot([1, 2, 3], [1, 4, 9])
axes[0, 0].set_title("Top left")

axes[0, 1].scatter([1, 2, 3], [3, 1, 2])
axes[0, 1].set_title("Top right")

axes[1, 0].bar(["A", "B", "C"], [3, 5, 2])
axes[1, 0].set_title("Bottom left")

axes[1, 1].hist([1, 2, 2, 3, 3, 3, 4, 4, 5], bins=5)
axes[1, 1].set_title("Bottom right")

plt.tight_layout()
plt.show()
Each axes[row, col] expression picks one subplot in a 2D grid — the first index selects the row, the second selects the column.
Each axes[row, col] expression picks one subplot in a 2D grid — the first index selects the row, the second selects the column.
Section 5 of 10

5 Saving figures

A figure that lives only in a popup window is gone the moment you close it. Most of the time you want a saved file you can drop into a slide, attach to an email, or include in a paper. Use plt.savefig("filename.ext") to export a clean, high-quality image.

  • File Formats – Use .png for slides and quick sharing. Use .pdf or .svg for journal submissions because they are vector formats that stay perfectly sharp at any zoom level. Never use .jpg for plots. It causes blurry text and artifacting.
  • Quality – Pass dpi=300 to guarantee print-quality resolution.
  • Cropping – Pass bbox_inches="tight" to automatically trim off excess white borders.
Matplotlib figures vanish when the popup window closes. To prevent this, save a clean image to disk.
Matplotlib figures vanish when the popup window closes. To prevent this, save a clean image to disk.

Two critical rules for saving:

  • Order matters – Always call plt.savefig() before plt.show(). If you call show() first, matplotlib often clears the canvas, resulting in a blank saved file.
  • Prevent memory leaks – If you are saving figures inside a loop (e.g., one plot per patient), add plt.close(fig) at the end of each loop iteration to free up your computer's memory.
How a matplotlib loop creates one figure per iteration, line by line.
How a matplotlib loop creates one figure per iteration, line by line.
Try it out

Try this snippet in the Python Scratchpad on the right.

Try this snippet
import matplotlib.pyplot as plt

patients = [
    ("P001", [82, 80, 78, 76]),
    ("P002", [91, 90, 88, 85]),
    ("P003", [70, 71, 70, 69]),
]

# One figure per patient -- save and close inside the loop.
for pid, weights in patients:
    fig, ax = plt.subplots()
    ax.plot([0, 3, 6, 12], weights, marker="o")
    ax.set_title(f"Weight trajectory -- {pid}")
    fig.savefig(f"{pid}_weight.png", dpi=300, bbox_inches="tight")
    plt.close(fig) # frees memory before the next iteration

Note: This will appear under the files tab in the Python Scratchpad. You can download the graphs to view.

Section 6 of 10

6 Putting it together

The tools across both parts of this module are usually used together. A typical short visualization workflow picks the right plot type from Part I, then labels and styles it, arranges it next to related panels, and saves the result. That is exactly what the worked example below does on a small patient dataset.

Worked example · Build a 2-panel figure of patient ages and BMI, save as PDF

Work through this example in three stages. You unlock each stage only after the tutor confirms the previous one. Each stage removes more of the scaffolding — by the end you are writing it yourself.

Problem: You have ten patients with ages and BMI values. Build a figure with two side-by-side panels: a histogram of ages on the left, and a scatter plot of BMI versus age on the right. Label both axes on each panel, give each panel a title, and save the result as a 300-dpi PDF called patient_overview.pdf.

Stage 1 · Study the solved example
Fully solved solution
import matplotlib.pyplot as plt

ages = [54, 61, 47, 72, 38, 65, 49, 58, 71, 44]
bmi  = [27.3, 24.1, 31.2, 22.8, 29.5, 26.0, 30.1, 25.4, 23.7, 28.2]

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

axes[0].hist(ages, bins=5, color="steelblue")
axes[0].set_xlabel("Age (years)")
axes[0].set_ylabel("Number of patients")
axes[0].set_title("Age distribution")

axes[1].scatter(ages, bmi, color="darkorange")
axes[1].set_xlabel("Age (years)")
axes[1].set_ylabel("BMI")
axes[1].set_title("BMI vs age")

plt.tight_layout()
plt.savefig("patient_overview.pdf", dpi=300, bbox_inches="tight")
plt.show()
Walk-through
  1. Create the two-panel figure with plt.subplots(1, 2) so the two panels sit side by side, and set figsize so the saved figure is wide enough to read each panel comfortably.
  2. Use axes[0].hist on the left panel because we are showing the distribution of one variable (ages), and pick a colour. Then set the axis labels and a title with the set_xlabel, set_ylabel, and set_title methods on that axes.
  3. Use axes[1].scatter on the right panel because we are showing a relationship between two variables (age and BMI), and label both axes the same way.
  4. Call plt.tight_layout() once at the end so the panel titles and labels do not bump into each other; this is much easier than fiddling with the spacing manually.
  5. Save before showing — plt.savefig with dpi=300 and bbox_inches="tight" produces a publication-ready PDF, and plt.show() at the very end is what displays the figure on screen.
Parsons problem · Build and save a labelled line plot of weight over time

All the lines you need are in the Line bank on the left — some may be distractors you should leave behind. Drag the lines you need into the Your solution column on the right, in the correct order, then click Check.

Task: Assemble a script that plots a single patient's weight at four follow-up visits, labels both axes (with units), gives the figure a title, and saves the result as a 300-dpi PNG before showing the figure on screen.

Line bank
  • import matplotlib.pyplot as plt
  • plt.scatter(months, weight_kg, marker="o")
  • plt.xlabel("Months since baseline")
  • plt.show()
  • plt.savefig("weight.png", dpi=300)
  • plt.show()
  • plt.plot(months, weight_kg, marker="o")
  • plt.title("Patient weight over follow-up")
  • months = [0, 3, 6, 12]
  • plt.legend("Patient A")
  • plt.savefig("weight.png", dpi=300, bbox_inches="tight")
  • plt.ylabel("Weight (kg)")
  • weight_kg = [82, 79, 75, 73]
Your solution
  • Drop lines here, in order.
Reflect

Generating a reflection question for you…

Section 7 of 10

7 Check your understanding

You have reached the end of the module. Try the same questions again — your answers here, paired with your pre-test answers, are how we measure what the module taught you. Answer every question and confidence rating, then click Submit and see results to view your score.

Post-test

You add label="Cohort 1" to plt.plot but the legend does not appear in your figure. What else do you need?

Post-test

You want to draw a horizontal reference line at y=7.0 across the whole plot to mark a clinical threshold. Which call adds it?

Post-test

Your x-axis shows the numbers 0, 1, 2, but you want the ticks to read "Baseline", "Week 1", "Week 2" instead. Which call does this?

Post-test

You write fig, axes = plt.subplots(1, 2) and try axes[0, 0].plot(...). Python raises IndexError: too many indices for array. What is the fix?

Post-test

Inside a figure made with fig, axes = plt.subplots(1, 2), you want to give the LEFT panel its own title. Which approach is correct?

Post-test

In a multi-panel figure, the subplot titles and axis labels overlap and collide with each other. Which single call fixes the spacing automatically?

Post-test

You call plt.savefig("figure.png") after plt.show() in a script and the saved PNG file is blank. What should you do?

Post-test

You are exporting a figure for a journal submission that must stay perfectly sharp at any zoom level. Which file format is the best choice?

Post-confidence

I can build a one-panel matplotlib figure with axis labels, a title, and a legend, and save it as a PNG.

Not at all confident
Fully confident
Post-confidence

I can build a two- or four-panel figure with plt.subplots, label every panel, and save the result as a 300-dpi PDF ready to drop into a paper or slide.

Not at all confident
Fully confident
Post-confidence

I can customize a plot with a threshold line, custom axis limits, and named tick labels, and style its lines with colours, markers, and line styles.

Not at all confident
Fully confident
Section 8 of 10

8 Your results

Here is how your post-test answers compare with your pre-test answers. The pre/post pairing is the most reliable way to see what this module actually taught you.

Your score

Submit the post-test to see your results.

Muddiest point

What is the one thing from this module that is still unclear to you?

Rate this module

Overall, how would you rate this module?

How likely are you to recommend this module to a peer? (0 = not at all, 10 = extremely likely)