Your First Data Visualisation
1 · The lesson
readA well-chosen chart answers a question faster than any table. Humans evolved to see patterns in shapes, not in spreadsheets. Whether you're explaining to a stakeholder or sanity-checking your own pipeline, plotting is the fastest path from raw data to "ah, that's what's going on."
Matplotlib is the workhorse — verbose, ubiquitous, and the substrate every other Python plotting library is built on. Once you can drive matplotlib, you can read any DS notebook in existence.
Note for the in-browser runner: matplotlib renders, but the image may not display in this Pyodide environment. Each example shows what the chart would look like in a comment.
1. The Three Charts That Pay Rent
Eighty percent of business plots are one of three shapes:
| Chart | What it shows | When to reach for it |
|---|---|---|
| Bar | One value per category | Comparing groups: sales by region, errors by service |
| Line | A value over an ordered axis (usually time) | Trends, sequences, before/after |
| Histogram | The distribution of one numeric column | "Is this column skewed? Bimodal? Outliers?" |
Master these three before touching pie charts, radars, or anything with three axes. Most pretty Plotly demos you scroll past on LinkedIn are misuse of charts these three would have answered better.
2. The Matplotlib Lifecycle
Every plot follows the same skeleton. Memorise it; vary the middle line.
import matplotlib.pyplot as plt plt.figure(figsize=(8, 4)) # 1. create a canvas plt.plot([1, 2, 3, 4], [10, 25, 18, 30]) # 2. add data plt.title("Sample Line Chart") # 3. label plt.xlabel("Quarter") plt.ylabel("Revenue (£k)") plt.grid(True, alpha=0.3) plt.savefig("sample.png", dpi=150) # 4. save (or plt.show()) plt.close() # 5. release the canvas # Chart preview (text): # Revenue (£k) # 30 | * # 25 | * # 18 | * # 10 | * # +------------------> Quarter # Q1 Q2 Q3 Q4
The convention is import matplotlib.pyplot as plt. The functional API (plt.plot, plt.title) is the easiest to learn; the object-oriented API (fig, ax = plt.subplots(); ax.plot(...)) is what you graduate to for complex figures.
3. Bar Chart — Categories
import matplotlib.pyplot as plt categories = ["Coffee", "Tea", "Cocoa", "Juice"] counts = [420, 280, 95, 160] plt.figure(figsize=(7, 4)) plt.bar(categories, counts, color="steelblue") plt.title("Drinks Sold This Week") plt.xlabel("Product") plt.ylabel("Units") plt.savefig("bars.png") plt.close() # Chart preview: # Units # 420 | ███ # 280 | ███ # 160 | ███ # 95 | ███ # +------------------- # Coffee Tea Cocoa Juice
Use plt.barh for horizontal bars when category names are long. Always sort by value unless the categories have a natural order (months, days of week).
4. Line Chart — Time / Sequence
import matplotlib.pyplot as plt months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"] sales = [120, 135, 128, 160, 175, 190] plt.figure(figsize=(7, 4)) plt.plot(months, sales, marker="o", linewidth=2) plt.title("Monthly Sales — 2026 H1") plt.xlabel("Month") plt.ylabel("Sales (£k)") plt.grid(True, alpha=0.3) plt.savefig("line.png") plt.close() # Chart preview: an upward trend from 120 to 190 with markers on each point.
Line charts imply an ordered x-axis. Categories without order (Coffee, Tea, Cocoa) belong in a bar chart, never a line.
5. Histogram — Distribution
import matplotlib.pyplot as plt import numpy as np rng = np.random.default_rng(42) heights = rng.normal(loc=170, scale=8, size=500) # 500 fake heights in cm plt.figure(figsize=(7, 4)) plt.hist(heights, bins=20, color="seagreen", edgecolor="black") plt.title("Distribution of Adult Heights") plt.xlabel("Height (cm)") plt.ylabel("Count") plt.savefig("hist.png") plt.close() # Chart preview: a bell curve centred near 170, tails near 150 and 190.
The number of bins matters — too few hides shape, too many shows noise. bins=20 is a sane default for sample sizes in the hundreds-to-thousands. Try bins="auto" and let matplotlib choose.
6. Subplots — Two Charts Side by Side
import matplotlib.pyplot as plt categories = ["A", "B", "C", "D"] counts = [12, 35, 22, 18] months = ["Jan", "Feb", "Mar", "Apr"] sales = [100, 120, 95, 140] fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # 1 row, 2 columns axes[0].bar(categories, counts, color="steelblue") axes[0].set_title("Category Counts") axes[0].set_xlabel("Category") axes[0].set_ylabel("Count") axes[1].plot(months, sales, marker="o", color="firebrick") axes[1].set_title("Monthly Sales") axes[1].set_xlabel("Month") axes[1].set_ylabel("Sales") fig.suptitle("Q1 Snapshot", fontsize=14) plt.tight_layout() plt.savefig("subplots.png") plt.close() # Chart preview: two charts side by side under one super-title.
When you switch to subplots you switch APIs — note the axes[0].set_title(...) style instead of plt.title(...). That's the object-oriented API, the one professional code is written in.
7. Scatter — Two Variables Together
import matplotlib.pyplot as plt import numpy as np rng = np.random.default_rng(0) hours = rng.uniform(0, 10, size=80) scores = 40 + hours * 6 + rng.normal(0, 8, size=80) plt.figure(figsize=(7, 5)) plt.scatter(hours, scores, alpha=0.7) plt.title("Study Hours vs Exam Score") plt.xlabel("Hours Studied") plt.ylabel("Score") plt.savefig("scatter.png") plt.close() # Chart preview: a positive, noisy upward cloud — more hours, higher scores.
Scatter plots reveal relationships. If the cloud is round and patternless, the variables are independent. If it slopes up or down, they're correlated. Reach for scatters during EDA whenever you wonder "does X relate to Y?".
8. Seaborn — Matplotlib With Better Defaults
# import seaborn as sns # import pandas as pd # # df = pd.DataFrame({ # "city": ["London", "Paris", "Berlin", "London", "Paris", "Berlin"], # "salary": [55_000, 72_000, 91_000, 38_000, 64_000, 88_000], # }) # # sns.boxplot(data=df, x="city", y="salary") # plt.title("Salary by City") # plt.savefig("seaborn_box.png")
Seaborn is built on top of matplotlib. It adds two things: prettier defaults and statistical chart types — boxplots, violinplots, regression plots, heatmaps — in one line. For exploratory work it's faster than matplotlib; for fine control you drop back to matplotlib underneath.
9. plt.show() vs plt.savefig()
- In a Jupyter notebook, the chart renders inline automatically. You can call
plt.show()for clarity but it's optional. - In a
.pyscript, nothing displays unless you callplt.show()(opens a window) orplt.savefig("out.png")(writes a file). - In pipelines and CI, always
savefigto a file path.show()blocks waiting for a human.
plt.savefig("chart.png", dpi=150, bbox_inches="tight")
setup added so this can run · defines plt
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) plt = _AutoMock('plt')
dpi=150 gives a sharp image; bbox_inches="tight" trims surrounding whitespace.
10. Chart Design — The 5-Minute Style Guide
A chart is for the reader, not the maker. Five rules:
1. Title says the takeaway in plain English. Not "Bar Chart" — "Coffee outsells tea 1.5× since March".
2. Axis labels include units. Revenue (£k), not just Revenue.
3. No chartjunk — drop 3D effects, gradients, drop shadows, decorative icons. Tufte called it "data-to-ink ratio". Maximise it.
4. Pick a colour-blind safe palette. viridis, cividis, and seaborn's defaults are designed for it. Avoid red-green pairs as semantic contrasts.
5. Pick the right chart. Pie charts work only for 2-5 categories that sum to a meaningful whole. Most pie charts should have been bars.
Common Mistakes
- No axis labels. A reader shouldn't have to read code to know what they're looking at.
- Pie chart with 12 slices. Use a sorted bar chart instead.
- Line chart over unordered categories. A line implies an axis you can move along. "Apples → Bananas → Cherries" doesn't have one.
- Forgetting
plt.close()in scripts. Eachplt.figure()consumes memory until the figure is closed. - Default colours over a colour-blind audience. Run your chart through a simulator before sharing.
- Saving with the default DPI. 100 dpi looks blurry in slides; use 150 or 200.
🎯 Your Turn — A Labelled Bar Chart
Given monthly sales numbers, produce a clean bar chart and save it to sales.png. Include a title, axis labels, and units.
Skeleton:
import matplotlib.pyplot as plt months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] sales = [100, 150, 90, 140, 180, 200, 230, 210, 175, 160, 140, 220] def plot_monthly_sales(months, sales, out_path="sales.png"): # TODO 1: create a figure with a sensible size # TODO 2: plt.bar(months, sales) # TODO 3: title, xlabel, ylabel, grid # TODO 4: savefig with dpi=150, bbox_inches="tight" # TODO 5: plt.close() to release the figure ... plot_monthly_sales(months, sales)
Hint 1 — The 4-line core
You needplt.figure(figsize=(10, 4)), plt.bar(months, sales), your three labels, and plt.savefig(out_path, dpi=150, bbox_inches="tight"). Anything beyond that is polish.
Hint 2 — Title that earns its keep
"Monthly Sales 2026" works. "Sales climbed 120% Jan→Jul, then drifted down" works better — title the finding, not the chart type.Show full solution
import matplotlib.pyplot as plt months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] sales = [100, 150, 90, 140, 180, 200, 230, 210, 175, 160, 140, 220] def plot_monthly_sales(months, sales, out_path="sales.png"): plt.figure(figsize=(10, 4)) plt.bar(months, sales, color="steelblue", edgecolor="black") plt.title("Monthly Sales 2026") plt.xlabel("Month") plt.ylabel("Sales (units)") plt.grid(True, axis="y", alpha=0.3) plt.savefig(out_path, dpi=150, bbox_inches="tight") plt.close() plot_monthly_sales(months, sales) print("Saved sales.png") # Chart preview: 12 vertical blue bars, lowest in March, highest in July.
Notice the grid is restricted to the y-axis — vertical gridlines on a categorical x-axis are noise. Small detail; reads cleaner.
What You Learned
- The three workhorse charts are bar (categories), line (sequence), histogram (distribution).
- Every matplotlib plot follows the same figure → plot → label → save lifecycle.
- Subplots put multiple charts in one figure — and pull you into the object-oriented API.
- Scatter plots reveal relationships between two numeric variables.
- Seaborn layers prettier defaults and statistical chart types on top of matplotlib.
- A good chart is one a reader understands without reading the code — title the finding, label the axes, drop the chartjunk.
Next: Exploratory Data Analysis — combining pandas and plotting into the disciplined "look at the data" workflow.