A chart is an argument
A chart isn't decoration - it's an argument made with ink. "Data roles pay more with seniority." "Postings peaked in spring." A good chart makes that claim in a second and lets the reader check it. The tool that draws it in Python is matplotlib, and here's the thing most people never learn: it's the engine under everything. seaborn calls it. pandas .plot() calls it. Every chart you've ever seen in a Python notebook came out of matplotlib. Learn its anatomy once and every other plotting library becomes a shortcut you understand, not magic you fear.
The Figure and Axes anatomy 5 min live
The #1 matplotlib confusion is that there are two ways to draw the same chart. Clear it up now and the docs stop feeling contradictory.
LiveTwo interfaces, one picture3 min▶
Matplotlib gives you two ways in, and mixing them is where beginners get lost:
- pyplot (stateful) -
plt.plot(...),plt.title(...). There's a hidden "current" figure and every command draws onto it. Quick for a one-off throwaway chart, clumsy the moment you want two plots. - Object-oriented (explicit) -
fig, ax = plt.subplots()hands you the Figure and the Axes as real objects. You callax.bar(...),ax.set_title(...)on the exact plot you mean. This is what pros use and what every serious codebase looks like.
The anatomy above is the whole vocabulary: a Figure is the canvas, an Axes is a single plot living on it (confusingly not the same as an "axis" - an Axes has an x axis and a y axis), and everything you label - title, ticks, legend, spines - hangs off that Axes.
Self-studyAlways use fig, ax = plt.subplots()2 min read▶
Why prefer this over plt.bar(...)? Because the moment you want a second plot, a shared legend, or small multiples, the object-oriented style just works - fig, (ax1, ax2) = plt.subplots(1, 2) and you have two named axes. The stateful version leaves you guessing which "current" figure a command lands on. Learn the explicit pattern first and you never unlearn a bad habit.
fig owns figure-wide things (size, saving, tight_layout). The ax owns the plot itself (the marks, title, labels, legend). When you're not sure where a method lives, ask: "is this about the whole canvas, or this one plot?"
The four charts you need 5 min live
You don't need thirty chart types. You need four, and the skill is matching each to the question it answers. Pick the wrong one and a true dataset tells a confusing story.
LiveWhich chart answers which question3 min▶
| Chart | Question it answers | On our data |
|---|---|---|
| Line | How does one number change over time? | postings per month across 2023-2024 |
| Bar | How do categories compare? | postings by role, or by location |
| Histogram | How is one numeric column distributed? | the spread of salary_year |
| Scatter | Do two numbers move together? | salary_year vs company size |
A common mistake: using a bar chart for salary spread. A bar of "average salary by role" hides that half the postings for a role pay wildly more or less than that average. Ask "distribution?" and reach for a histogram - it's the difference between a headline that's true and one that's technically-an-average.
Self-studyThe method names on an Axes2 min read▶
ax.plot(x, y)- lineax.bar(categories, heights)- vertical bars (ax.barhfor horizontal, better for long role names)ax.hist(values, bins=30)- histogramax.scatter(x, y)- scatter
Same four verbs, same object. Once fig, ax = plt.subplots() is muscle memory, switching chart types is just switching the method you call on ax.
Labels, styles, subplots, saving 4 min live
A chart without labels is a puzzle. Here's the small set of methods that turn a raw plot into something you'd publish - and how to save it right.
LiveLabel it, size it, save it2 min▶
For more than one plot, fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 5)) gives you a grid of Axes - "small multiples" that let a reader compare panels side by side. Always finish with tight_layout() so nothing overlaps.
Self-studyWhy PNG and SVG, and why 300 dpi2 min read▶
- PNG at dpi=300 is print / publication quality - crisp in slides and on LinkedIn, not the blurry 72-dpi screenshot look.
- SVG is a vector: it stays razor-sharp at any zoom because it stores shapes, not pixels. Perfect when someone blows your chart up on a projector.
- Saving both is the workspace convention - PNG for the quick paste, SVG for anything that might get resized or printed.
An unlabeled chart is a lie by omission. A bar climbing from 3 to 5 means nothing until the y axis says "salary in thousands USD" or "postings per month". Every axis gets units before the chart leaves your notebook - that one habit separates a screenshot from a finding people trust.
A labeled bar of the top roles ★ 8 min · everyone codes
From your clean frame, count postings per job title and keep the top 10.
Build the figure explicitly, draw the bars, rotate the labels so long titles don't collide.
Title it, label both axes, then save a 300 dpi PNG.
ha="right", or flip to a horizontal bar with ax.barh(top.index[::-1], top.values[::-1]) - reversed so the biggest bar sits on top. For role names, horizontal usually reads better.
A salary histogram with a median line ★ 8 min · everyone codes
Take the salaried subset (the ~30% of postings with a real salary_year) and draw its distribution.
Choose a sensible bin count, then drop a vertical line at the median with axvline to anchor the eye.
Read the shape out loud: is it symmetric, or does a long right tail pull the mean above the median?
Salary data is almost always right-skewed - a long tail of a few very high offers drags the mean well above the typical role. The histogram makes that visible, and the median line shows where most people actually land. This is exactly the chart that keeps your Session 8 report from claiming a misleadingly high "average" salary.
Two panels side by side ★ 6 min · everyone codes
Make a 1x2 grid: postings by location on the left, the salary distribution on the right.
Each panel is its own Axes - draw on ax1 and ax2 independently.
tight_layout, then save both a PNG and an SVG for the report.
(ax1, ax2). For a 2x2 you'd get ((ax1, ax2), (ax3, ax4)), or capture the whole thing as axes and index it. Same Figure, many Axes - that's all small multiples are.
This week ◐ 30 min total
- Read the matplotlib pyplot tutorial once, top to bottom. You've seen the object-oriented style live - now see how the docs frame both interfaces.
- Rebuild each demo chart on YOUR data - the bar, the histogram with a median line, and the 1x2 panel - and save each as a PNG. Get the fig, ax pattern into your fingers.
- Read McKinney chapter 9 (Plotting and Visualization) - it's the same anatomy from a second angle, and it locks it in.
- Make one chart you'd actually post. Pick the single most interesting thing in your frame, chart it, label it honestly, save it at 300 dpi. That's the seed of your Session 8 report.
Three questions before you go 🎯 ◐ 90 seconds
1 · Why prefer fig, ax = plt.subplots() over calling plt.plot() directly?
The object-oriented style hands you the Figure and Axes as real objects, so multiple plots, shared legends, and small multiples just work. plt.plot draws onto a hidden "current" figure you have to keep track of.
2 · Which chart shows the distribution of a single numeric column like salary_year?
A histogram bins one numeric column and shows how values spread - skew, clusters, outliers. A bar of the average hides all of that behind a single number.
3 · Why save with savefig(dpi=300) in both PNG and SVG?
300 dpi keeps the raster crisp in print and slides; SVG is a vector that never blurs when resized. Saving both covers the quick paste and the projector blow-up.
Official sources covered
This session teaches the working content of the official matplotlib docs and Wes McKinney's open-access Python for Data Analysis (3e). Interactive galleries and the deep API stay on those platforms - links provided.