Matplotlib Fundamentals — Plots & Customization
plt.subplots(), and save a finished chart to a file with plt.savefig().
The Figure/Axes Model
Every chart Matplotlib draws lives inside two objects: a Figure, which is the whole canvas — the window or image file — and one or more Axes, which are the individual plotting areas inside that canvas (confusingly, an "Axes" is a full chart with its own x-axis and y-axis, not a single axis line). The standard way to create both together is plt.subplots().
import matplotlib.pyplot as plt # fig = the whole canvas, ax = one plotting area inside it fig, ax = plt.subplots(figsize=(8, 5)) ax.plot([1, 2, 3, 4, 5], [10, 15, 13, 18, 22]) ax.set_title("Weekly Revenue") plt.show()
fig, ax = plt.subplots() and calls plt.plot(...), plt.title(...) directly on the plt module itself — this is "pyplot style," and it quietly plots onto whatever the "current" figure and axes happen to be. It's fine for a single quick chart, but the object-oriented style used in this lesson — creating fig, ax explicitly and calling methods on ax — scales far better once you're building subplot grids or reusable plotting functions.Two methods you'll use on almost every ax object: ax.set_title() and, as you'll see next, ax.set_xlabel() / ax.set_ylabel() for labeling each axis of the chart.
Line Plots with plt.plot()
ax.plot(x, y) connects a sequence of (x, y) points with a line — the natural choice whenever there's a meaningful order to the data, like values changing over time.
weeks = [1, 2, 3, 4, 5, 6] revenue = [210, 245, 198, 260, 288, 305] fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(weeks, revenue, color="#4f9eff", linewidth=2, marker="o", label="Revenue") ax.set_title("Weekly Revenue") ax.set_xlabel("Week") ax.set_ylabel("Revenue (PKR '000)") ax.legend() plt.show()
.py script, plt.show() opens a window with the chart. In a Jupyter notebook, the chart usually renders automatically below the cell, but calling plt.show() explicitly is still good habit — it also suppresses the noisy <Axes: ...> object repr that would otherwise print above the chart.Bar Charts with plt.bar()
Bar charts compare a numeric value across discrete categories — ax.bar(categories, values) draws one bar per category, with height equal to the value.
categories = ["Electronics", "Clothing", "Home", "Sports"] values = [4200, 3100, 2600, 1800] fig, ax = plt.subplots(figsize=(7, 5)) ax.bar(categories, values, color="#2de8c0") ax.set_title("Sales by Category") ax.set_ylabel("Units Sold") plt.show()
ax.bar() draws vertical bars; ax.barh() draws them horizontally, which is often easier to read when category names are long — no rotated x-axis labels needed.Scatter Plots with plt.scatter()
Scatter plots show the relationship between two numeric variables as individual points — one point per row of data. They're the go-to chart for spotting correlation, clusters, or outliers.
import numpy as np rng = np.random.default_rng(42) ad_spend = rng.uniform(10, 100, 40) orders = ad_spend * 2.3 + rng.normal(0, 15, 40) fig, ax = plt.subplots(figsize=(7, 5)) ax.scatter(ad_spend, orders, c="#a78bfa", alpha=0.75, edgecolors="white") ax.set_title("Ad Spend vs. Orders") ax.set_xlabel("Ad Spend (PKR '000)") ax.set_ylabel("Orders") plt.show()
alpha below 1.0 makes each point semi-transparent, so areas where many points overlap look visibly darker — a cheap way to see point density without a completely different chart type.Histograms with plt.hist()
A histogram shows how one numeric column is distributed — it buckets values into ranges ("bins") and draws a bar for how many values fall in each bin. It answers "what does this column's spread look like?" rather than comparing categories.
order_values = rng.normal(loc=500, scale=80, size=1000) fig, ax = plt.subplots(figsize=(7, 5)) ax.hist(order_values, bins=30, color="#fbbf24", edgecolor="black") ax.set_title("Distribution of Order Values") ax.set_xlabel("Order Value (PKR)") ax.set_ylabel("Frequency") plt.show()
Titles, Labels, Legends & Styling
Every chart so far has used set_title(), set_xlabel()/set_ylabel(), and legend() — these plus color and line-style keyword arguments are what turn a bare plot into something someone else can actually read without you standing next to them explaining it.
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"] sales_2023 = [180, 195, 170, 210, 225, 240] sales_2024 = [220, 230, 205, 260, 288, 305] fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(months, sales_2023, color="#8ca8d8", linestyle="--", linewidth=2, marker="s", label="2023") ax.plot(months, sales_2024, color="#2de8c0", linestyle="-", linewidth=2.5, marker="o", label="2024") ax.set_title("Sales: 2023 vs. 2024", fontsize=14, fontweight="bold") ax.set_xlabel("Month") ax.set_ylabel("Sales (PKR '000)") ax.legend(loc="upper left") ax.grid(True, alpha=0.3) plt.show()
color accepts named colors ("red"), hex codes ("#4f9eff"), or shorthand letters ("b" for blue). linestyle takes "-" (solid), "--" (dashed), "-." (dash-dot), or ":" (dotted). marker takes shorthand like "o" (circle), "s" (square), "^" (triangle). All three work on ax.plot(); ax.bar() and ax.scatter() use color/c the same way but don't take a line style.Laying Out Multiple Charts with a Subplot Grid
plt.subplots() also accepts a number of rows and columns, returning a grid of Axes objects instead of just one — perfect for putting several related charts side by side.
fig, axes = plt.subplots(2, 2, figsize=(10, 8)) axes[0, 0].plot(weeks, revenue, color="#4f9eff") axes[0, 0].set_title("Line") axes[0, 1].bar(categories, values, color="#2de8c0") axes[0, 1].set_title("Bar") axes[1, 0].scatter(ad_spend, orders, c="#a78bfa") axes[1, 0].set_title("Scatter") axes[1, 1].hist(order_values, bins=20, color="#fbbf24") axes[1, 1].set_title("Histogram") plt.tight_layout() plt.show()
plt.subplots(2, 2), axes is a 2D NumPy array, indexed axes[row, col]. With a single row or column, e.g. plt.subplots(1, 3), axes is a flat 1D array, indexed just axes[i]. With plt.subplots() and no arguments, you get one plain Axes object, no indexing at all. plt.tight_layout() is worth calling before plt.show() on any multi-subplot figure — it fixes titles and labels overlapping between neighboring subplots.Saving Figures with plt.savefig()
Once a chart looks right, fig.savefig() (or the equivalent plt.savefig()) writes it to a file — PNG, PDF, SVG, and more, chosen automatically from the file extension.
fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(months, sales_2024, color="#2de8c0", marker="o") ax.set_title("2024 Sales") # Save BEFORE plt.show() — some backends clear the figure after showing it fig.savefig("sales_2024.png", dpi=300, bbox_inches="tight") plt.show()
dpi=300 (dots per inch) gives a crisp, print-quality image — the on-screen default is much lower resolution. bbox_inches="tight" trims excess whitespace around the chart so titles and labels aren't cut off at the edge of the saved file.Lesson Summary
Let's recap everything you learned in this lesson:
fig, ax = plt.subplots() return?bins keyword argument belongs to which chart function, and what does it control?ax.legend() do?Put every chart type from this lesson into a single figure.
Using
weeks/revenue, categories/values, ad_spend/orders, and order_values from this lesson: (1) build a 2×2 subplot grid with plt.subplots(2, 2, figsize=(10, 8)), (2) put the line chart, bar chart, scatter plot, and histogram each in their own cell with a title, (3) call plt.tight_layout(), then (4) save the whole grid to "weekly_report.png" at 300 dpi before calling plt.show().
Rules: Use the object-oriented style throughout — call methods on the individual
axes[row, col] objects, not on plt directly.
💡 Show hints if you're stuck
fig, axes = plt.subplots(2, 2, figsize=(10, 8))- Each cell:
axes[0, 0].plot(...),axes[0, 1].bar(...),axes[1, 0].scatter(...),axes[1, 1].hist(...) - Save with
fig.savefig("weekly_report.png", dpi=300, bbox_inches="tight")beforeplt.show()