AI EngineeringZero to ProductionHome·About·What’s new·Contact
Data & App Building · Chapter B2

Data Visualization with Matplotlib and Seaborn

A number in a table hides; a picture reveals. This chapter is practical plotting: Matplotlib's figure/axes model (the foundation everything sits on), Seaborn's statistical charts straight from a DataFrame, and — crucially — how to choose the right chart and not lie with it.

⏱️ ~55 min📈 Hands-on🎯 Beginner→Intermediate

Learning objectives

  • Explain Matplotlib's Figure/Axes model and the two API styles.
  • Make the core chart types and know which question each answers.
  • Use Seaborn to plot statistical charts directly from a DataFrame.
  • Choose the right chart for the data — and avoid misleading ones.
  • Save/return charts for reports, apps, and LLM pipelines.
Builds on B1Visualization consumes the clean DataFrames you produced in B1 — you plot the result of a groupby, not raw mess. Matplotlib underlies almost all Python plotting (including Seaborn and pandas' own .plot()), and it's pre-installed in the code-execution sandbox (C2/tool-use), so agents use it to generate charts too.

The Matplotlib mental model: Figure & Axes essential

Here's the moment most people give up on Matplotlib. They copy a snippet, it works; they change one thing, it breaks; they add a second plot and suddenly labels land on the wrong chart. So they paste random plt. lines until something sticks, and quietly decide Matplotlib is just fiddly. It isn't. Almost every one of those frustrations traces back to a single missing idea — and once you have it, the whole library snaps into a shape you can reason about.

The idea is a two-layer split. A Figure is the whole canvas — think of it as the sheet of paper you're drawing on. An Axes is one plot sitting on that paper, with its own x-axis, y-axis, title, and data. The confusing part is the name: "Axes" does not mean the two little x/y lines you'd expect — it means an entire chart. One Figure can hold many Axes (a 2×2 grid of subplots is one Figure with four Axes), and you almost never draw on the Figure directly — you draw on an Axes.

An analogy that sticks: the Figure is an art gallery wall, and each Axes is a framed picture hung on it. You don't paint on the wall; you paint inside a frame. When you say ax.plot(...) you're painting inside that specific frame, so there's never any doubt where the line goes. Compare that to the beginner habit of calling plt.plot(...), which paints on "whichever frame is currently active" — fine with one picture, chaos with several.

The common mistake this section inoculates you against: treating plt as the thing you draw on. Learners write plt.plot(), plt.title(), plt.xlabel() and it feels natural — until they build two charts and the title lands on the wrong one, because plt was pointing at a different "current" Axes than they thought. The fix is to hold a handle to the exact Axes you mean and call methods on it. That's why the very next line you'll see everywhere is fig, ax = plt.subplots(): it hands you both objects — the canvas and the one plot — so you're never guessing which chart you're editing. Internalize "Figure = canvas, Axes = one plot, I always draw on an Axes" and the rest of this chapter is just vocabulary.

Almost all Matplotlib confusion comes from missing one idea. A Figure is the whole canvas; an Axes is a single plot on it (with its own x/y axis). You draw on Axes. A figure can hold several Axes (subplots).

Figure (the canvas) Axes 1 Axes 2 you call methods on an Axes: ax.plot(...), ax.set_title(...) Figure = canvas, Axes = one plot. You almost always create both with fig, ax = plt.subplots() and then call methods on ax — ax.plot(), ax.bar(), ax.set_title(). Grasp this and Matplotlib stops feeling random; miss it and you'll fight the API forever.
🗺️ How to read this diagram

This is the one idea that makes Matplotlib click. A Figure is the whole canvas (the sheet of paper); an Axes is a single plot drawn on it, with its own x-axis and y-axis. "Axes" is confusing at first — it does not mean the x/y lines; it means one whole chart.

  • The big outer box is the Figure — the container. One figure can hold several plots.
  • Each inner box is an Axes — one individual chart. Here the figure holds two: Axes 1 (drawn with a line) and Axes 2 (drawn with bars).
  • The little axis lines and the line/bars inside each box are what you actually draw. You put them there by calling methods on that Axes — the caption shows ax.plot(...) and ax.set_title(...).
  • The normal way to get both at once is fig, ax = plt.subplots(): fig is the canvas, ax is the one plot on it.

In short: Figure = the page, Axes = one chart on the page. You spend almost all your time calling ax.something(...) to draw on a single Axes.

Two API styles — pick the explicit one essential

Matplotlib has two front doors, and picking the wrong one is why so much tutorial code feels like black magic. The stakes are real: the style you learn first becomes the style you reach for under deadline pressure, and one of these two styles quietly falls apart exactly when your work gets serious — when you need two charts, or wrap plotting in a function, or hand a figure to an app. Choose well now and you never have to unlearn a habit.

The first style is pyplot, the plt.-everything approach: plt.plot(x, y), then plt.title(...), then plt.show(). It's seductive because it's short. But notice what's missing — you never named the chart you're drawing on. Pyplot keeps a hidden global variable called the "current axes," and every plt. call secretly edits that. It's like a workshop with one shared workbench: fine when you're the only person building one thing, a disaster the moment there are two projects and you can't tell which bench a tool just modified.

The second style is object-oriented (explicit): fig, ax = plt.subplots() hands you the Figure and the Axes as named variables, and you call methods on them — ax.plot(x, y), ax.set_title(...). It's one extra line, and in return the ambiguity vanishes: every command names the exact chart it touches. There is no "current" anything to lose track of.

The mistake to avoid is assuming the two styles are interchangeable dialects you can freely mix. You can mix them, and the result is the worst of both worlds — some lines edit the object you're holding, others edit the hidden global, and when they disagree you get labels on the wrong plot with no error message to explain why. The honest rule is simple: pick the explicit style and stay in it. The table below lines the two up side by side so you can see that the object-oriented version costs almost nothing and buys you every real-world scenario.

pyplot — implicit plt.plot(x, y) hidden "current axes" which plot? whichever was last active object-oriented — explicit ax.plot(x, y) the ax you named no ambiguity — this exact plot Explicit beats implicit. plt.plot() edits a hidden "current axes" you never named; ax.plot() edits the exact Axes you hold. With one plot the difference is invisible — with two, the implicit style puts labels on the wrong chart.
🗺️ How to read this diagram

Compare the two panels left and right — same drawing command, two very different targets.

  • The left (red) side is pyplot: plt.plot(x, y) flows down into a hidden "current axes" that Matplotlib tracks for you. You never named it, so with several plots open you can't be sure which one you just edited.
  • The right (green) side is the object-oriented style: ax.plot(x, y) flows into the ax you named yourself, so the target is never in doubt.
  • The dashed divider separates the two front doors; the arrows are the same action landing on a guessed target (left) versus a named one (right).

In short: Both styles "draw a line" — but only the explicit ax tells you which plot the line lands on. Pick it and stay in it.

pyplot (state machine)Object-oriented (explicit)
Looks likeplt.plot(x, y); plt.title(...)fig, ax = plt.subplots(); ax.plot(x, y)
Draws on"current" axes (implicit)the ax you hold (explicit)
Fine forquick one-off plots in a notebookeverything real — subplots, apps, functions
Prefer fig, ax = plt.subplots()The plt.-everything style relies on a hidden "current axes" and breaks down the moment you have multiple plots or return a figure from a function. The explicit object-oriented style — hold fig and ax, call methods on them — is unambiguous and scales. Learn it first; it's the same effort and saves confusion later.

Lab B2.1 · Core charts in Matplotlib essential

Here's a liberating fact: you already know 90% of Matplotlib. People assume they have to memorize dozens of chart functions, but the truth is that every chart you'll draw follows the same four-step recipe — make a Figure and Axes, draw on the Axes, label it, save or return it. Only step two changes: swap ax.plot for ax.bar or ax.hist and you've got a different chart. Learn the recipe once and the "which function?" question shrinks to a small vocabulary list.

Why does that recipe matter more than the individual functions? Because the recipe is where the judgment lives. Choosing ax.plot versus ax.bar isn't a syntax decision — it's a claim about what your data is. A line says "these points are ordered and connected, watch the trend." Bars say "these are separate categories, compare their sizes." A histogram says "forget individual rows, here's the shape of one column's values." Reach for the wrong verb and you've mislabeled your data before you've written a single word of caption.

Think of the Axes as a chef's plate you're building up in layers: first the base (the data — ax.plot), then the seasoning (title and axis labels), then plating for service (savefig to disk). The lab below runs that recipe twice — once for a single line chart, once for a two-panel figure — so you see that going from "one plot" to "a grid of plots" is the same code, just with more Axes handed back to you by plt.subplots(1, 2).

The classic beginner mistake here is skipping the seasoning: drawing the data and calling it done. An unlabeled chart is not evidence, it's decoration — the reader has no idea whether the y-axis is dollars or percent, whether up is good or bad, what the thing even measures. Every real chart gets a title and both axes labelled, every time; it's the cheapest credibility you'll ever buy. Watch for set_title, set_xlabel, and set_ylabel in the code below — they're not optional polish, they're part of the recipe.

1 · createfig, ax = subplots() 2 · drawax.plot / bar / hist 3 · labelset_title / labels 4 · save/returnsavefig / return fig only this step changes per chart type same recipe for a line, a bar, or a 2-panel grid Every chart is the same four steps. Create → draw → label → save/return. Only step 2 changes — swap ax.plot for ax.bar or ax.hist and you have a different chart from the identical recipe.
🗺️ How to read this diagram

Read it left to right — it's the recipe every chart in this chapter follows, in order.

  • Box 1 (create) makes the canvas and the plot in one line: fig, ax = plt.subplots().
  • Box 2 (draw) is the only step that changes between chart types — the amber note points here: ax.plot for a line, ax.bar for bars, ax.hist for a histogram.
  • Box 3 (label) adds the title and axis labels — not optional polish, part of the recipe.
  • The green box 4 is the payoff: savefig to disk or return fig to a caller — the same finish whether it's one plot or a grid.

In short: Learn the four-step recipe once; the "which function?" question shrinks to picking the verb in step 2.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Lab B2.1

Requires: pip install matplotlib

charts.pyimport matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(months, revenue, marker="o")     # line: trend over time
ax.set_title("Monthly revenue")
ax.set_xlabel("month"); ax.set_ylabel("$")
fig.savefig("revenue.png", dpi=150, bbox_inches="tight")

# subplots: two Axes on one Figure
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.bar(regions, totals)          # bar: compare categories
ax2.hist(latencies, bins=30)      # histogram: see a distribution
▶ How this works

This is the core Matplotlib pattern you'll reuse everywhere: make a Figure and Axes, draw on the Axes, label it, then save it. The second half shows how to put two plots side by side on one figure.

  1. fig, ax = plt.subplots(figsize=(7, 4)) creates the canvas (fig) and one plot (ax) on it. figsize is the size in inches (width, height).
  2. ax.plot(months, revenue, marker="o") draws a line chart — x-values months against y-values revenue, with a dot at each point. A line is the right choice for a trend over time.
  3. ax.set_title(...), ax.set_xlabel(...), and ax.set_ylabel(...) add the title and axis labels. Always label — an unlabeled chart can mean anything.
  4. fig.savefig("revenue.png", dpi=150, bbox_inches="tight") writes the picture to a file. dpi sets sharpness; bbox_inches="tight" trims extra whitespace around the edges.
  5. The second block asks for a grid of plots: plt.subplots(1, 2, ...) means 1 row, 2 columns, so you get back two Axes — ax1 gets a bar chart (ax1.bar, to compare categories) and ax2 gets a histogram (ax2.hist(..., bins=30), to see a distribution).

What the output means: Two PNG-able figures: a labelled revenue line chart saved as revenue.png, and a second figure showing a bar chart and a histogram side by side.

Try this: Change marker="o" to marker="s" (squares) or drop it entirely, and change bins=30 to bins=5 on the histogram — fewer bins means chunkier bars and a coarser view of the distribution.

pandas plots tooA DataFrame has .plot() built in (it uses Matplotlib): by_region.plot.bar(ax=ax). That's the fastest path from a B1 groupby to a chart — pass it the ax you made so you keep control of the figure.

Lab B2.2 · Seaborn: statistical charts from a DataFrame intermediate

If Matplotlib is a box of art supplies, Seaborn is a photo booth. With raw Matplotlib, drawing "average revenue per region, with error bars" means: group the DataFrame yourself, compute the means, compute the standard errors, position the bars, draw the whiskers, pick the colors. That's a dozen lines of plumbing for a chart every analyst makes fifty times a week. Seaborn's whole reason to exist is to collapse that into one line — and understanding why it can is the key to using it well.

The trick is that Seaborn was designed around a different input than Matplotlib. Matplotlib wants arrays — "here are the x-values, here are the y-values, plot them." Seaborn wants a whole DataFrame plus the names of columns — "here's the data, use the region column for x and the total column for y, and figure out the rest." Because it can see the entire frame, it can do the grouping and the statistics for you: it knows how to split rows by category, average within each group, and draw the spread. You stopped describing pixels and started describing relationships between columns.

An analogy: Matplotlib is like giving someone turn-by-turn driving directions ("go 200 meters, turn left, draw a bar 40 pixels tall"). Seaborn is like giving them a destination ("show me total by region") and trusting the built-in navigation. The single most important word to learn is hue — the parameter that says "and split everything by this category using color." One hue="region" turns a plain distribution into a side-by-side comparison across regions, no loop, no manual color-cycling. It's the closest thing plotting has to a superpower.

The mistake to watch for is reaching for Seaborn for the wrong job. Seaborn shines for statistical, exploratory charts drawn from a tidy DataFrame — distributions, group comparisons, correlations. It is not the tool when you need pixel-perfect control over a bespoke figure for a slide, or a chart type it doesn't offer; there you drop down to the Matplotlib underneath it (Seaborn returns ordinary Axes, so you can). And it expects tidy data — one row per observation, one column per variable, the clean shape you built in B1. Hand it a messy pivot table and it can't do its job. Read the lab as "same DataFrame, five questions, five one-liners" — that density is the payoff.

Seaborn sits on Matplotlib and is built for DataFrames: you pass the frame plus column names, and it handles the statistics and styling. It's the fast path for the charts analysts actually make.

Matplotlib · you prepare x = [...], y = [...] you group + compute first ax.bar(x, y) arrays in → pixels out Seaborn · it prepares data=df,x="region", y="total" group · average · error bars one line → statistical chart Different input, less plumbing. Matplotlib wants finished arrays — you group and compute yourself. Seaborn takes the whole DataFrame plus column names and does the grouping, statistics, and error bars for you, collapsing a dozen lines into one.
🗺️ How to read this diagram

Compare the two columns top to bottom — the same bar-by-region chart, two amounts of work.

  • The left side is Matplotlib: it wants finished arrays (x and y), so you do the grouping and averaging before you ever call ax.bar.
  • The right side is Seaborn: you hand it the whole DataFrame plus the column names (data=df, x="region", y="total").
  • The amber box is the work Seaborn does for you — group by category, average within each, draw error bars — because it can see the entire frame. The arrows show that one call flows straight to a finished statistical chart.

In short: Matplotlib = "here are the numbers, plot them"; Seaborn = "here's the data, name the columns" — and it does the statistics itself.

Lab B2.2

Requires: pip install seaborn

seaborn_charts.pyimport seaborn as sns

sns.barplot(data=df, x="region", y="total")     # aggregates + error bars for you
sns.histplot(data=df, x="latency", bins=30)      # distribution
sns.scatterplot(data=df, x="price", y="qty", hue="region")  # relationship, colored by group
sns.boxplot(data=df, x="region", y="total")     # compare distributions across groups
sns.heatmap(df.corr(numeric_only=True), annot=True)  # correlations between numeric columns
▶ How this works

Seaborn is built for DataFrames: instead of handing it arrays, you hand it the whole frame and the names of the columns to use. It then does the grouping, the math, and the styling for you. Every line here is a different chart type from the same df.

  1. The shared shape is sns.SOMECHART(data=df, x="col", y="col") — pass the DataFrame, then name the columns for the x and y axes.
  2. sns.barplot(..., x="region", y="total") groups by region and shows the average total per region, with little error bars for the spread — all computed automatically.
  3. sns.histplot(..., x="latency", bins=30) shows the distribution of one column; sns.scatterplot(..., x="price", y="qty") plots each row as a dot to reveal a relationship between two numbers.
  4. hue="region" is the superpower: it splits the chart by category using color, turning one plot into a comparison across groups.
  5. sns.boxplot(...) compares the shape of a distribution across groups, and sns.heatmap(df.corr(numeric_only=True), annot=True) draws a colored grid of how strongly the numeric columns correlate (annot=True prints the number in each cell).

What the output means: Five statistical charts drawn straight from df — bar, histogram, scatter, box, and correlation heatmap — with grouping and aggregation handled for you.

Try this: Add hue="region" to the histplot line and watch it split into one colored distribution per region. Then remove annot=True from the heatmap to see the grid without the printed numbers.

Seaborn = "pass the DataFrame, name the columns"Where Matplotlib wants arrays, Seaborn wants data=df, x="col", y="col" — and it does the grouping, aggregation, and error bars for you. The hue="group" parameter is a superpower: it splits any chart by a category with color, turning one plot into a comparison. Reach for Seaborn for statistical/exploratory charts; drop to Matplotlib when you need pixel control.

Choosing the right chart intermediate

You can write flawless plotting code and still produce a useless chart. This is the part nobody warns beginners about: the syntax is the easy 10%; the hard 90% is picking a chart type that actually answers the question you're asking. A pie chart of twelve slices, a line connecting unordered categories, a single bar hiding a wildly bimodal distribution — all perfectly valid code, all quietly failing the reader. The stakes are that a well-drawn wrong chart is more dangerous than an ugly one, because its polish earns trust it hasn't earned.

The reframe that fixes this: a chart type is an answer to a specific kind of question, so start from the question, not the picture. Before you type anything, finish the sentence "I want the reader to see ___." If the blank is how something changes over time, that's a line — a line's whole job is to connect ordered points so your eye rides the trend. If it's how categories stack up against each other, that's a bar — because humans are startlingly good at comparing lengths side by side. If it's the shape of one variable's values — where they cluster, how they skew, whether there are outliers — that's a histogram or box or violin. If it's whether two numbers move together, that's a scatter. Match the verb of your question to the chart and you're almost always right.

Why does the human eye care which chart? Because our visual system is precise about some encodings and terrible at others. We judge length and position accurately — which is why bars and dot positions work — but we're clumsy at judging angles and areas, which is exactly what a pie chart asks of us. That single fact is why the table below quietly recommends "bar, usually not pie" for parts-of-a-whole: not fashion, but the wiring of your reader's brain. A pie forces people to compare wedge angles they can't compare; a bar chart of the same shares lets them just read off the lengths.

The mistake this section is really about is defaulting to whatever chart is easy or pretty instead of what the question demands — grabbing a pie because it looks friendly, or a bar of averages because it's one line, when the reader actually needed to see the spread. The strongest tell that you've chosen well is that the chart has one clear message a stranger could state in a sentence. Read the mapping below not as rules to memorize but as a translation table from questions you'll genuinely ask to the picture that answers each one.

The hardest part isn't the code — it's picking a chart that answers your question honestly. Match the chart to the question.

"I want to show ___" change over time compare categories shape of one variable two numbers linked? Line Bar Scatter Histogram / box Start from the question, not the picture. Finish "I want the reader to see ___" and the chart falls out: over time → line, compare categories → bar, shape of a variable → histogram/box, two numbers linked → scatter.
🗺️ How to read this diagram

Read it top-down: the question at the top branches to the kind of thing you're asking, and each of those points to the chart that answers it.

  • The top box is the sentence you finish before choosing a chart: "I want the reader to see ___."
  • The middle row names the four common answers — a change over time, a comparison of categories, the shape of one variable, or whether two numbers are linked.
  • The green boxes are the matching chart types: line, bar, histogram/box, and scatter. Follow the connector down from your question to land on the right picture.

In short: Match the verb of your question to the chart — trend→line, compare→bar, spread→histogram, link→scatter — and you're almost always right.

Your questionChartWhy
How does X change over time?LineConnects ordered points; shows trend
How do categories compare?BarLength is easy to compare across groups
What's the distribution of X?Histogram / box / violinShows spread, skew, outliers
Is X related to Y?ScatterReveals correlation/clusters
How do many variables correlate?HeatmapMatrix of pairwise relationships
Parts of a whole (few, sum to 100%)Bar (usually) not pieHumans compare lengths better than angles
Averages hide the story — show the distributionA bar of group means can look identical for wildly different data. If the shape matters — outliers, skew, bimodality — reach for a histogram, box, or violin plot instead of a single bar. "The average is 50" is very different from "half are 0 and half are 100." The chart you choose decides which truth the reader sees.

Lab B2.3 · Not lying with charts intermediate

The scariest thing about a misleading chart is that you can draw one by accident. Nobody sets out to deceive when they nudge a y-axis or pick a color scheme — but the reader can't tell an honest mistake from a deliberate spin, and either way they walk away believing something false. The stakes climb the moment a chart leaves your screen: it goes in a report, a board deck, a customer email, and its claim outlives your intentions. Learning the handful of ways charts distort is less about ethics lectures and more about not embarrassing yourself with a graph that says something you didn't mean.

The mental frame that makes this click: a chart is an argument, not a neutral mirror of the data. Every chart makes a claim — "this went up," "these are different," "these two move together" — and the visual choices are the rhetoric of that argument. The single most common distortion is the truncated axis: a bar chart's whole persuasive power is that the reader reads length as value, so if you start the y-axis at 90 instead of 0, a trivial 2% difference balloons into a bar that looks twice as tall as its neighbor. You didn't touch the numbers — you changed the ruler, and the ruler is where the reader's eye actually looks.

Here's the intuition for why the zero baseline is non-negotiable for bars specifically: the bar is a physical stand-in for the quantity, so its bottom must sit at zero or the length stops meaning what the reader thinks it means. (This is why line charts, which encode value by position rather than length, can sometimes justify a non-zero axis — but bars never can.) The same family of tricks includes cherry-picking a flattering date range, and the notorious dual y-axis, where two unrelated series are scaled independently until they appear to dance together — a correlation manufactured purely by the choice of scales.

The mistake to internalize isn't malice, it's carelessness: accepting whatever axis limits the library picks, letting a template's default scale ride, forwarding a chart you didn't sanity-check. The professional habit is the opposite — default to honest scales, start bar axes at zero, label the units so the reader can check you, and ask "could someone read the opposite true thing from this?" The diagram below is the canonical demonstration: the exact same two numbers drawn twice, once with a truncated axis and once from zero, telling flatly opposite stories. Study it until the trick is impossible to un-see.

A chart is an argument. Small choices can mislead — sometimes accidentally. Professional plotting means making the honest version.

✗ truncated axis y starts at 90 ✓ zero-based axis y starts at 0 Same data, opposite impression. A y-axis that starts at 90 makes a 2% difference look enormous; a zero-based axis shows it's tiny. Truncating axes, cherry-picking ranges, or using dual axes to force a correlation are the classic ways charts mislead. Default to honest scales and label everything.
🗺️ How to read this diagram

This picture shows the single most common way a chart lies: where the y-axis starts. Both sides plot the exact same two numbers — only the axis differs — yet they tell opposite stories.

  • The left chart (✗) has a y-axis that starts at 90, not 0. That chops off the bottom of the bars, so a tiny 2% difference in the numbers looks like a huge gap.
  • The right chart (✓) has a y-axis that starts at 0. Now you see the full height of each bar, and the real difference — small — is obvious.
  • The lesson: for bar charts, the bar's length is how the reader judges the value, so starting anywhere but zero exaggerates. Truncated axes, cherry-picked ranges, and forced dual axes are the classic tricks.

In short: Same data, opposite impression. For bars, start the y-axis at zero and label everything — that's the honest default.

Honest-charting ruleWhy
Start bar-chart y-axes at zeroBar length encodes value; a truncated axis exaggerates
Label axes, units, and the titleAn unlabeled chart can mean anything
Don't over-plot / rainbow everythingClutter hides the signal; use color with purpose
Show the data's real shapeDistribution over a lone average when spread matters
One clear message per chartA chart trying to say five things says nothing

Lab B2.4 · Charts for reports, apps & LLM pipelines advanced

Every plotting tutorial ends with plt.show() — and that's exactly the habit that will break your first real application. plt.show() pops up a window and blocks until a human closes it. That's fine at your desk; it's a hung process inside a web server, a frozen batch job in a nightly report, or a timed-out tool call inside an agent. The stakes here are the difference between "a chart I looked at" and "a chart my system can actually use" — and the fix is a small shift in who's in charge of the picture.

The reframe is this: don't display a chart, produce one and hand it back. Instead of a function that draws and shows, write a function that builds a Figure and returns the object — then let whoever called it decide what to do. A report pipeline saves it to a PNG. A Streamlit or Gradio app (B4) passes it to a display component. An agent's code sandbox turns it into an image file to send back to the model. Same figure, three destinations, and none of them wants a blocking pop-up window.

The analogy is a kitchen. plt.show() is a chef who insists on personally carrying every plate to the table and standing there until you finish — the kitchen grinds to a halt. Returning a Figure is a chef who plates the dish and puts it in the pass; the waiter (your caller) takes it wherever it needs to go. That separation — build here, deliver there — is what lets one chart-making function serve a file, a webpage, and an LLM without changing a line. Notice in the lab how make_chart creates its own fig, ax and passes ax=ax to Seaborn: that's the function staying in full control of its figure rather than scribbling on some global "current" plot a caller might not expect.

The mistake, then, is thinking of a chart as something you watch rather than an artifact you hand off. The tell that you've got it right: your plotting code never calls plt.show() and never assumes a screen is attached. This matters far beyond tidiness — it's the reason the exact Seaborn snippets from earlier in this chapter can run untouched inside the Data Analyst Agent's sandbox and come back as a PNG. "Return a Figure" is a small discipline that quietly makes your visualization work portable across reports, apps, and agents alike.

Where visualization plugs into the rest of the course: charts get saved for reports, returned to a web app (B4), or generated by an agent in the code sandbox.

make_chart(df)builds fig, returns it reportsavefig(.png) web app · B4st.pyplot(fig) agent · C2PNG → model plt.show() blocks all three ✗ Return a Figure, don't show it. One function builds the figure and hands it back; the caller decides — save to a report, display in a Streamlit/Gradio app, or return a PNG from an agent. A blocking plt.show() would break every one of these.
🗺️ How to read this diagram

Read it top-down: one function at the top fans out to three places the same figure can go.

  • The top box is make_chart(df): it builds a Figure and returns the object instead of displaying it.
  • The three green boxes are the destinations, all fed by the same returned figure — a report (savefig to PNG), a web app (st.pyplot(fig) in B4), and an agent (a PNG sent back to the model, C2).
  • The red box at the bottom is the anti-pattern: a blocking plt.show() inside the function would hang a server, freeze a batch job, and time out an agent — it breaks all three destinations at once.

In short: Build here, deliver there — return fig and let the caller save, display, or ship it; never plt.show() inside the function.

Lab B2.4

Illustrative fragment — defines demo values / files are needed before this runs standalone.

deliver.py# save to a file (reports, email, artifacts)
fig.savefig("report.png", dpi=150, bbox_inches="tight")

# return the figure object to a web framework (Streamlit/Gradio, B4)
def make_chart(df) -> "plt.Figure":
    fig, ax = plt.subplots()
    sns.barplot(data=df, x="region", y="total", ax=ax)
    return fig          # st.pyplot(fig) / Gradio Plot component renders it

# in an agent's code-execution tool, the generated PNG comes back as a file (C2)
▶ How this works

The point of a chart is to deliver it somewhere: a saved image for a report, or a live figure handed to a web app or an agent. This fragment shows both — and why returning a figure (not calling plt.show()) is the shape the rest of the course needs.

  1. fig.savefig("report.png", dpi=150, bbox_inches="tight") writes the figure to a file — the way you attach a chart to a report or an email.
  2. def make_chart(df) -> "plt.Figure": is a function that builds a chart and hands it back. Inside it makes its own fig, ax, draws a Seaborn bar plot onto that ax, and then return fig — it returns the figure object instead of showing it.
  3. Passing ax=ax to sns.barplot tells Seaborn to draw on the exact Axes you created, so the function stays in control of its figure.
  4. Returning the figure lets a caller decide what to do with it: a web framework shows it (st.pyplot(fig) in Streamlit, a Plot component in Gradio, B4), or an agent's code sandbox turns it into a PNG file (C2).

What the output means: A reusable make_chart(df) that returns a Figure you can either save to disk or hand to an app/agent to display — no plt.show() needed.

Try this: Never put plt.show() inside a function like this — it tries to pop up a window and blocks apps and agents. Build the figure, return fig, and let the caller display or save it.

Agents make charts tooThe code-execution tool (tool-use in C2, and the sandbox in Claude Skills) ships with Matplotlib/Seaborn pre-installed. So when the Data Analyst Agent (P5) answers "plot revenue by region", it writes exactly this Seaborn code, runs it in the sandbox, and returns the PNG. The visualization skill you're learning is the same one the agent uses — which is why "return a Figure" matters as much as "show a plot."

Common pitfalls advanced

Almost every visualization bug you'll hit is one of a handful of repeat offenders — which is good news, because it means you can inoculate yourself against most of them in one sitting. The stakes are that these mistakes don't announce themselves: your code runs, a chart appears, and only later does someone point out that the axis lied, the label was missing, or the "distribution" was actually a single misleading average. Knowing the usual suspects turns a vague "something's off" into an instant diagnosis.

It helps to notice these pitfalls come in two flavors, because the fixes are different in kind. The first flavor is mechanical — fighting the tool. Battling the plt. state machine, forgetting labels, passing raw messy data straight into a plot: these are habits, and the cure is a better default habit (hold fig, ax; label everything; clean in Pandas first). The second flavor is perceptual — misleading the human eye even with correct code: a truncated bar axis, a bar of means over bimodal data, a twelve-slice pie. Those aren't bugs the interpreter can catch; they're failures of judgment about how a reader sees. Both flavors show up in the table below, and it's worth reading each row as "the trap, then the reflex that avoids it."

mechanical · fighting the tool plt. state machine missing labels raw messy data in perceptual · fooling the eye truncated bar axis mean hides distribution many-slice pie fix = a better habit fix = judgment, no error fires Two kinds of pitfall, two kinds of fix. Mechanical traps (state machine, missing labels, messy data) are cured by a better habit. Perceptual traps (truncated axis, mean hiding spread, giant pie) draw fine and throw no error — only judgment catches them.
🗺️ How to read this diagram

Read it as two side-by-side buckets — every pitfall lands in one of them, and the bucket tells you the kind of fix.

  • The left (amber) column is mechanical bugs — you're fighting the tool: the plt. state machine, forgetting labels, feeding in raw messy data.
  • The right (red) column is perceptual traps — the code is correct but the picture misleads: a truncated axis, a mean that hides the spread, a pie with too many slices.
  • The two bottom lines are the key difference: mechanical traps have a habit-based fix (hold fig, ax, label, clean first), while perceptual traps throw no error — only your judgment about how a reader sees will catch them.

In short: Left column = fix the habit; right column = fix the judgment. The dangerous ones are on the right because nothing warns you.

PitfallFix
Fighting the plt. state machineUse fig, ax = plt.subplots() and call methods on ax
Truncated bar-chart axesStart at zero; don't exaggerate differences
Unlabeled axes/title/unitsLabel everything — a chart must stand alone
Bar of means hiding the distributionUse histogram/box/violin when shape matters
Pie chart with many slicesPrefer a bar chart; lengths beat angles
Passing raw messy data to a plotClean/aggregate in Pandas first (B1)
Reaching for Matplotlib for a stat chartSeaborn from the DataFrame is faster & cleaner

Exercises advanced

Exercise B2.1 — Chart a groupby

Context: A group-by result from the data lesson is only an answer once someone can see it. Three views of the same summary — a comparison, a trend, and a statistical chart — cover most of what a report needs.

Your task: Take a group-by result and make three charts of it with the explicit fig, ax style — a bar, a line, and one Seaborn statistical chart — labelling every axis and title, and save one as a PNG.

Requirements:

  • Reuse a real group-by result as the data
  • Make three charts with the explicit Figure/Axes API: a bar, a line, and a Seaborn statistical chart
  • Give every chart a clear title and label both axes
  • Save at least one chart as a PNG file

💡 Hint: Match each chart to its job — bar to compare groups, line for a trend, Seaborn for the statistical view — and drive them all through ax.

Exercise B2.2 — Fix a misleading chart

Context: The judgment that separates a chart from a lie is seeing how the same data can read two ways. Building the dishonest version on purpose, then the honest one, makes the difference impossible to un-see.

Your task: Deliberately make a misleading chart (a truncated y-axis, or a bar of means over bimodal data), then make the honest version and write one sentence on how the reader's takeaway changed.

Requirements:

  • Build the misleading chart first (truncated axis, or a mean-bar over split data)
  • Build the honest counterpart from the same data
  • Only the framing changes — the underlying numbers are identical
  • Write one sentence on what the reader now correctly sees
  • Note which specific choice (zero baseline, histogram) fixed the distortion

💡 Hint: For a truncated axis the fix is a zero baseline; for a mean-bar over bimodal data the fix is a histogram that reveals the two clusters.

Show what to look for

The truncated-axis version makes a trivial difference look dramatic; the zero-based version shows it's small. The mean-bar hides that the data is split; the histogram reveals two clusters. In both, the data is identical — only the honest chart lets the reader see what's true.

Exercise B2.3 — Return a Figure

Context: Apps and agents don't want a chart splashed to the screen — they want a Figure object they can save or hand to a display function. Returning the figure instead of calling show() is the shape the rest of the module depends on.

Your task: Write a function make_chart(df) -> Figure that builds and returns a matplotlib Figure without calling plt.show() inside it, then confirm you can both save it and pass the returned figure to a display function.

Requirements:

  • The function returns a Figure object
  • It does not call plt.show() internally
  • The returned figure can be saved with savefig
  • The same returned figure can be handed to a display function (e.g. st.pyplot)

💡 Hint: Build the figure with fig, ax = plt.subplots() and simply return fig — let the caller decide whether to save or display it.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · A labeled line chart the explicit wayBeginner

Context: The plt.plot global-state style works until you need two charts in one figure, then it fights you. The explicit Figure/Axes API scales cleanly and never depends on hidden state — it's the habit worth building first.

Your task: Using the explicit fig, ax = plt.subplots() API, plot monthly values as a line chart with a title and labels on both axes.

Requirements:

  • Create the figure and axes explicitly with plt.subplots(), not plt.plot global state
  • Plot the monthly series on the axes (a marker helps read the points)
  • Set a title and label both the x and y axes
  • Render or save the figure (e.g. fig.savefig(...) or plt.show())

💡 Hint: Everything hangs off the ax object — ax.plot, ax.set_title, ax.set_xlabel — so the chart never relies on which plot was 'current'.

Show solution

Needs matplotlib + a display/file. Prefer the explicit fig, ax API — it scales to subplots and never depends on hidden global state:

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr"]
values = [10, 14, 9, 18]

fig, ax = plt.subplots()
ax.plot(months, values, marker="o")
ax.set_title("Monthly active users")
ax.set_xlabel("Month")
ax.set_ylabel("Users (thousands)")
fig.savefig("mau.png", dpi=120, bbox_inches="tight")   # or plt.show()

Always label axes and title — an unlabeled chart isn’t evidence, it’s decoration.

Exercise 2 · Pick the right chart for the dataIntermediate

Context: Choosing the wrong chart type quietly misleads even with correct data. The chart should be dictated by the question you're answering, not by what looks nice.

Your task: For three situations — revenue share across five product lines, a distribution of request latencies, and a year of daily signups — name the right chart for each and say why, then show sample code for one.

Requirements:

  • Revenue share across categories → a bar chart, because lengths are easy to compare (not a pie)
  • Distribution of latencies → a histogram (or box/violin) to show spread and outliers a mean hides
  • Trend over a year → a line chart with time on the x-axis
  • Give a one-line justification tying each choice to comparison / distribution / trend
  • Provide runnable sample code for at least one (e.g. the bar chart)

💡 Hint: Map the question to the chart: comparison wants bars, spread wants a histogram, change-over-time wants a line.

Show solution
  • (a) Share across categories: a bar chart (not a pie) — bars let the eye compare lengths precisely; pies are hard to read past 3–4 slices.
  • (b) Distribution: a histogram (or box/violin) — you want to see spread, skew, and outliers, which a single mean hides.
  • (c) Trend over time: a line chart — time on x, value on y, continuity shows the trend.

Needs matplotlib + display for the bar example:

import matplotlib.pyplot as plt
lines = ["A", "B", "C", "D", "E"]; rev = [30, 22, 18, 15, 5]
fig, ax = plt.subplots()
ax.bar(lines, rev)
ax.set_ylabel("Revenue share (%)"); ax.set_title("Revenue by product line")
plt.show()

Match the chart to the question: comparison → bars, distribution → histogram, trend → line.

Exercise 3 · A statistical chart from a DataFrame with SeabornAdvanced

Context: Seaborn maps DataFrame columns straight onto statistical charts, so you can see a distribution per category in a couple of lines — and a box plot exposes the outlier that a bar-of-means would bury.

Your task: Use Seaborn to draw a box plot of a numeric metric across a category directly from a tidy DataFrame — for example latency by endpoint.

Requirements:

  • Feed a tidy DataFrame to Seaborn, mapping the category to x and the metric to y
  • Use a box plot (sns.boxplot) so spread and outliers are visible
  • Give the chart a descriptive title
  • Render the figure (needs seaborn + matplotlib + a display)
  • Confirm the box plot surfaces an outlier that a mean would hide

💡 Hint: Pass data=df, x=..., y=... straight to sns.boxplot — Seaborn handles the aggregation and drawing from the column names.

Show solution

Needs seaborn + matplotlib + a display. Seaborn maps DataFrame columns straight to a statistical chart:

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df = pd.DataFrame({
    "endpoint": ["/a", "/a", "/a", "/b", "/b", "/b"],
    "latency":  [120, 130, 400, 90, 95, 88],
})

ax = sns.boxplot(data=df, x="endpoint", y="latency")
ax.set_title("Latency distribution by endpoint")
plt.show()

The box plot exposes the /a outlier (400ms) that a bar-of-means would hide — the point of a distributional view.

Exercise 4 · Don’t lie with chartsExpert

Context: Two of the most common ways a chart misleads are a truncated y-axis that exaggerates tiny differences and dual axes that imply a correlation that isn't there. Knowing the honest fix is the judgment that separates a chart from a lie.

Your task: Explain the truncated-y-axis and dual-axis pitfalls, then show the honest fix in code — a bar chart whose y-axis starts at zero so a small gap looks small.

Requirements:

  • Explain why a truncated y-axis exaggerates small differences on a bar chart
  • Explain why dual y-axes can imply a false correlation between unrelated series
  • Show a bar chart with the y-axis baseline at 0 (ax.set_ylim(0, ...))
  • Contrast it with the misleading truncated version in a comment
  • State the rule: bar length encodes value, so the axis must start at 0

💡 Hint: For bars the length is the value, so setting a zero baseline is the fix; note what the misleading set_ylim would have been.

Show solution

Truncated y-axis exaggerates small differences; start bar-chart axes at 0. Dual y-axes can make two unrelated series look correlated — avoid unless the reader clearly understands both scales.

Needs matplotlib + a display. Honest bar chart with a zero baseline:

import matplotlib.pyplot as plt

labels = ["Q1", "Q2", "Q3"]; vals = [98, 99, 100]

fig, ax = plt.subplots()
ax.bar(labels, vals)
ax.set_ylim(0, 110)      # honest: baseline at 0, so 98 vs 100 looks like ~2%
ax.set_title("Quarterly score (true scale)")
plt.show()
# Misleading version would be: ax.set_ylim(97, 100)  -> a 2% gap looks huge

The rule: for bars, the length encodes the value, so the axis must start at 0 or you distort the comparison.

Exercise 5 · One figure, saved for a reportProfessional

Context: A figure destined for a report needs more than a plot — it needs multiple panels in one image, a shared title, and print-resolution output that doesn't clip its own labels. That polish is the line between a draft and something you paste in.

Your task: Produce a report-ready figure with two subplots — a trend and a distribution — sharing one overall title, saved to a file at print resolution.

Requirements:

  • Create a single figure with two subplots via plt.subplots(1, 2, ...)
  • Configure each axes independently (one line/trend, one histogram)
  • Add a shared overall title with fig.suptitle(...)
  • Save to a file at print resolution (a higher dpi) with bbox_inches="tight"
  • Use tight_layout() so labels don't clip

💡 Hint: subplots(1, 2) hands back an array of axes you configure separately, then tight_layout plus bbox_inches="tight" keep the saved image clean.

Show solution

Needs matplotlib + file output. subplots returns an array of axes you configure independently, then save the whole figure:

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr"]; signups = [10, 14, 9, 18]
latencies = [120, 130, 400, 90, 95, 88, 110, 100]

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.plot(months, signups, marker="o"); ax1.set_title("Signups"); ax1.set_xlabel("Month")
ax2.hist(latencies, bins=6); ax2.set_title("Latency distribution"); ax2.set_xlabel("ms")
fig.suptitle("Weekly report", fontweight="bold")
fig.tight_layout()
fig.savefig("report.png", dpi=150, bbox_inches="tight")

tight_layout() + bbox_inches="tight" stop labels from clipping — the difference between a draft and something you paste into a doc.

Exercise 6 · A reusable chart helper for a pipelineIndustry scenario

Context: An automated report pipeline needs a chart function it can call repeatedly in a headless job without a display — and without leaking a figure into memory on every run. That means the Agg backend and disciplined cleanup.

Your task: Wrap chart creation in a function that takes a DataFrame plus column names, writes a PNG, and depends on no global pyplot state so an automated report pipeline can call it in a loop.

Requirements:

  • Select a headless backend (matplotlib.use("Agg")) so no display is needed
  • The function takes the DataFrame, the category and value column names, and an output path
  • Create a fresh fig, ax per call — no reliance on global pyplot state
  • Label the axes and write the PNG to the given path
  • Close the figure (plt.close(fig)) so a long-running job doesn't leak memory, and return the path

💡 Hint: The Agg backend plus a per-call figure and plt.close is exactly what makes matplotlib safe inside a repeated, headless report job.

Show solution

Needs matplotlib. Create a fresh Figure per call (no global state), and close it so a long-running pipeline doesn’t leak figures:

import matplotlib
matplotlib.use("Agg")            # headless backend: no display needed in a pipeline
import matplotlib.pyplot as plt

def save_bar(df, cat_col, val_col, out_path, title=""):
    # Write a bar chart of val_col by cat_col to out_path (PNG). Returns out_path.
    fig, ax = plt.subplots()
    ax.bar(df[cat_col].astype(str), df[val_col])
    ax.set_xlabel(cat_col); ax.set_ylabel(val_col); ax.set_title(title)
    fig.tight_layout()
    fig.savefig(out_path, dpi=120, bbox_inches="tight")
    plt.close(fig)               # release memory in a batch job
    return out_path

# usage (in a pipeline):
# import pandas as pd
# df = pd.DataFrame({"region": ["US","EU"], "rev": [250,120]})
# save_bar(df, "region", "rev", "rev.png", "Revenue by region")

Agg backend + plt.close are what make matplotlib safe inside a headless, repeated report job.

✓ Checkpoint — you can move on when you can…

  • Explain Figure vs Axes and use the explicit fig, ax style.
  • Make line, bar, histogram, and scatter charts.
  • Use Seaborn to plot statistical charts straight from a DataFrame.
  • Pick the right chart for a question and avoid misleading ones.
  • Save a chart and return a Figure for an app or agent.
🏗️ Toward the capstone & projectsThe Data Analyst Agent (P5) ends its answers with a chart — generated by exactly the Seaborn code here, run in the code sandbox, returned as an image. And an O4 monitoring dashboard for the capstone is just these charts over the four signals (cost, latency, quality, safety). Clean it in B1, chart it in B2, then B3/B4 serve it. Next: serve it over an API →

Knowledge check check yourself

✓ Knowledge check

In Matplotlib, what is the difference between a Figure and an Axes, and which do you draw on?

Show answer
A Figure is the whole canvas (which can hold several plots); an Axes is a single plot on it with its own x/y axis. You draw on an Axes — calling methods like ax.plot() and ax.set_title(), usually obtained via fig, ax = plt.subplots().
✓ Knowledge check

Why should bar-chart y-axes start at zero, and what does a truncated axis do to the reader?

Show answer
Because a bar's length encodes its value, so starting the axis anywhere but zero exaggerates differences — a y-axis starting at 90 can make a tiny 2% difference look enormous, misleading the reader.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in