Matplotlib
Matplotlib is Python's foundational plotting library. Its pyplot module provides a MATLAB-like interface, and virtually every other Python visualization library (Seaborn, Pandas .plot()) is built on top of it.
Two Interfaces
| Interface | Style | When to use |
|---|---|---|
| pyplot (state-based) | plt.plot(), plt.title() — acts on the "current" figure | Quick single plots, interactive exploration |
| Object-oriented | fig, ax = plt.subplots() then ax.plot() | Recommended — explicit, required for subplots, reusable |
Anatomy of a Figure
FIGURE (the whole canvas)
└── AXES (one plot area; a figure can hold many)
├── Axis (x and y, with limits/scale)
├── Ticks and tick labels
├── Title, xlabel, ylabel
├── Legend
├── Spines (the four border lines)
└── Artists (lines, bars, markers, text, patches)
Basic Plots
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
months = ["Jan","Feb","Mar","Apr","May","Jun"]
revenue = [420, 465, 448, 512, 590, 634]
cost = [310, 330, 340, 355, 390, 410]
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(months, revenue, marker="o", linewidth=2, color="#168B99", label="Revenue")
ax.plot(months, cost, marker="s", linewidth=2, linestyle="--",
color="#f59e0b", label="Cost")
ax.fill_between(months, cost, revenue, alpha=0.15, color="#10b981", label="Profit")
ax.set_title("Revenue vs Cost — H1 2026", fontsize=14, fontweight="bold")
ax.set_xlabel("Month", fontsize=11)
ax.set_ylabel("Amount (Rs lakh)", fontsize=11)
ax.legend(loc="upper left", frameon=False)
ax.grid(axis="y", alpha=0.3)
ax.set_axisbelow(True)
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
plt.show()
The Main Chart Types
np.random.seed(42)
fig, axes = plt.subplots(2, 3, figsize=(17, 9))
# 1. LINE — trend over time
x = np.linspace(0, 10, 100)
axes[0,0].plot(x, np.sin(x), color="#168B99", linewidth=2)
axes[0,0].set_title("Line Plot — trends")
# 2. BAR — category comparison
cats = ["Electronics", "Clothing", "Books", "Home"]
vals = [4200, 3100, 1800, 2600]
bars = axes[0,1].bar(cats, vals, color="#10b981", edgecolor="white")
axes[0,1].set_title("Bar Chart — comparison")
axes[0,1].tick_params(axis="x", rotation=30)
for bar in bars: # direct labels
axes[0,1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 60,
f"{int(bar.get_height())}", ha="center", fontsize=9)
# 3. HORIZONTAL BAR — long category names
axes[0,2].barh(cats, vals, color="#6366f1")
axes[0,2].set_title("Horizontal Bar")
# 4. HISTOGRAM — distribution (Unit 2)
data = np.random.normal(65, 12, 500)
axes[1,0].hist(data, bins=25, color="#f59e0b", edgecolor="white")
axes[1,0].axvline(data.mean(), color="#ef4444", linestyle="--",
label=f"mean = {data.mean():.1f}")
axes[1,0].set_title("Histogram — distribution")
axes[1,0].legend()
# 5. SCATTER — relationship (Unit 2)
xs = np.random.uniform(1, 12, 100)
ys = 5.5 * xs + 30 + np.random.normal(0, 6, 100)
axes[1,1].scatter(xs, ys, alpha=0.6, c=ys, cmap="viridis", s=45)
axes[1,1].set_title("Scatter Plot — relationship")
# 6. PIE — composition (few slices only)
axes[1,2].pie(vals, labels=cats, autopct="%1.1f%%", startangle=90,
colors=["#168B99", "#10b981", "#f59e0b", "#6366f1"],
explode=[0.05, 0, 0, 0])
axes[1,2].set_title("Pie Chart — composition")
plt.tight_layout()
plt.show()
Subplots and Layout
# Grid of subplots
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(11, 8),
sharex=False, sharey=False)
axes = axes.flatten() # convenient 1-D access
for i, ax in enumerate(axes):
ax.plot(np.random.randn(50).cumsum(), color=f"C{i}")
ax.set_title(f"Series {i+1}")
fig.suptitle("Four Random Walks", fontsize=15, fontweight="bold")
plt.tight_layout()
plt.show()
# Unequal layouts with GridSpec
fig = plt.figure(figsize=(12, 7))
gs = fig.add_gridspec(2, 3, hspace=0.35, wspace=0.3)
ax_big = fig.add_subplot(gs[0, :]) # top row, all columns
ax1 = fig.add_subplot(gs[1, 0])
ax2 = fig.add_subplot(gs[1, 1])
ax3 = fig.add_subplot(gs[1, 2])
ax_big.plot(months, revenue, marker="o", color="#168B99", linewidth=2)
ax_big.set_title("Main Metric — Revenue")
for ax, (title, data) in zip([ax1, ax2, ax3], [
("Distribution", np.random.normal(0, 1, 200)),
("Categories", None),
("Correlation", None),
]):
if data is not None:
ax.hist(data, bins=20, color="#10b981")
else:
ax.bar(cats[:3], vals[:3], color="#f59e0b")
ax.set_title(title, fontsize=10)
plt.show()
Customisation Reference
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(months, revenue,
color="#168B99", # line colour (name, hex, or "C0")
linewidth=2.5, # thickness
linestyle="-", # '-', '--', '-.', ':'
marker="o", # 'o','s','^','D','*','x','+'
markersize=8,
markerfacecolor="white",
markeredgewidth=2,
alpha=0.9,
label="Revenue")
ax.set_title("Fully Customised Chart", fontsize=15, fontweight="bold", pad=15)
ax.set_xlabel("Month", fontsize=12)
ax.set_ylabel("Revenue (Rs lakh)", fontsize=12)
ax.set_ylim(0, 700) # axis limits
ax.set_yticks(range(0, 701, 100)) # explicit ticks
ax.tick_params(axis="both", labelsize=10)
ax.legend(loc="lower right", fontsize=11, frameon=True, shadow=False)
ax.grid(True, axis="y", alpha=0.3, linestyle=":")
ax.set_axisbelow(True)
# Annotate a specific point
peak_idx = int(np.argmax(revenue))
ax.annotate(f"Peak: {revenue[peak_idx]}",
xy=(peak_idx, revenue[peak_idx]),
xytext=(peak_idx - 1.5, revenue[peak_idx] + 60),
arrowprops=dict(arrowstyle="->", color="#ef4444", linewidth=1.5),
fontsize=11, color="#ef4444", fontweight="bold")
ax.axhline(np.mean(revenue), color="gray", linestyle="--", alpha=0.7,
label=f"Average = {np.mean(revenue):.0f}")
plt.tight_layout()
plt.show()
Colour and Style Reference
print(plt.style.available)
# ['ggplot', 'seaborn-v0_8', 'fivethirtyeight', 'bmh', 'dark_background', ...]
plt.style.use("seaborn-v0_8-whitegrid") # apply a style globally
# plt.style.use("default") # reset
# Colormaps
# Sequential: viridis, plasma, Blues, YlOrRd -> ordered data (low to high)
# Diverging: coolwarm, RdBu, seismic -> data with a meaningful midpoint
# Qualitative: tab10, Set1, Set2, Paired -> unordered categories
Plotting Directly from Pandas
df = pd.DataFrame({
"month": months, "revenue": revenue, "cost": cost,
}).set_index("month")
fig, axes = plt.subplots(2, 2, figsize=(13, 8))
df.plot(ax=axes[0,0], marker="o", title="Line — .plot()")
df.plot(kind="bar", ax=axes[0,1], title="Grouped Bar", rot=0)
df.plot(kind="bar", stacked=True, ax=axes[1,0], title="Stacked Bar", rot=0)
df.plot(kind="area", ax=axes[1,1], alpha=0.6, title="Area Chart")
plt.tight_layout()
plt.show()
# Available kinds: 'line','bar','barh','hist','box','kde','density',
# 'area','pie','scatter','hexbin'
Dual Axes and Secondary Scales
fig, ax1 = plt.subplots(figsize=(9, 5))
ax1.bar(months, revenue, color="#168B99", alpha=0.75, label="Revenue")
ax1.set_ylabel("Revenue (Rs lakh)", color="#168B99", fontsize=12)
ax1.tick_params(axis="y", labelcolor="#168B99")
ax2 = ax1.twinx() # share the x-axis, new y-axis
margin = [(r - c) / r * 100 for r, c in zip(revenue, cost)]
ax2.plot(months, margin, color="#ef4444", marker="o", linewidth=2.5, label="Margin %")
ax2.set_ylabel("Profit Margin (%)", color="#ef4444", fontsize=12)
ax2.tick_params(axis="y", labelcolor="#ef4444")
ax2.set_ylim(0, 50)
ax1.set_title("Revenue and Profit Margin", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
Use dual axes sparingly — they can imply a relationship between two series that does not exist. When in doubt, use two stacked subplots sharing the x-axis instead.
Saving Figures
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(months, revenue, marker="o", color="#168B99")
ax.set_title("Revenue Trend")
fig.savefig("revenue.png", dpi=300, bbox_inches="tight") # raster, presentations
fig.savefig("revenue.pdf", bbox_inches="tight") # vector, print/reports
fig.savefig("revenue.svg", bbox_inches="tight") # vector, web
fig.savefig("revenue_transparent.png", dpi=300, transparent=True,
bbox_inches="tight", facecolor="none")
plt.close(fig) # free memory when generating many figures in a loop
| Parameter | Purpose |
|---|---|
dpi | Resolution — 300 for print, 100 for screen |
bbox_inches="tight" | Trims excess whitespace |
transparent=True | Transparent background for slides |
format | Inferred from the extension; PNG/JPG raster, PDF/SVG vector |
A Complete Analytical Dashboard
np.random.seed(42)
sales = pd.DataFrame({
"date": pd.date_range("2026-01-01", periods=180),
"revenue": (np.random.randn(180).cumsum() * 200 + 5000).clip(1000),
"category": np.random.choice(["Electronics","Clothing","Books","Home"], 180),
"units": np.random.randint(5, 60, 180),
})
fig = plt.figure(figsize=(15, 9))
gs = fig.add_gridspec(3, 3, hspace=0.45, wspace=0.3)
# KPI row
kpis = [("Total Revenue", f"Rs {sales['revenue'].sum()/1e5:.1f}L", "#168B99"),
("Total Units", f"{sales['units'].sum():,}", "#10b981"),
("Avg Daily Rev", f"Rs {sales['revenue'].mean():,.0f}", "#f59e0b")]
for i, (label, value, colour) in enumerate(kpis):
ax = fig.add_subplot(gs[0, i])
ax.text(0.5, 0.62, value, ha="center", fontsize=22, fontweight="bold", color=colour)
ax.text(0.5, 0.28, label, ha="center", fontsize=11, color="gray")
ax.axis("off")
# Trend
ax_trend = fig.add_subplot(gs[1, :])
ax_trend.plot(sales["date"], sales["revenue"], color="#168B99", alpha=0.4, linewidth=1)
ax_trend.plot(sales["date"], sales["revenue"].rolling(14).mean(),
color="#ef4444", linewidth=2.5, label="14-day moving average")
ax_trend.set_title("Daily Revenue Trend", fontweight="bold")
ax_trend.legend(); ax_trend.grid(alpha=0.3)
# Category breakdown
ax_cat = fig.add_subplot(gs[2, 0])
cat_rev = sales.groupby("category")["revenue"].sum().sort_values()
ax_cat.barh(cat_rev.index, cat_rev.values, color="#10b981")
ax_cat.set_title("Revenue by Category", fontsize=11)
# Distribution
ax_dist = fig.add_subplot(gs[2, 1])
ax_dist.hist(sales["revenue"], bins=25, color="#6366f1", edgecolor="white")
ax_dist.set_title("Revenue Distribution", fontsize=11)
# Monthly
ax_month = fig.add_subplot(gs[2, 2])
monthly = sales.set_index("date").resample("ME")["revenue"].sum()
ax_month.bar(range(len(monthly)), monthly.values, color="#f59e0b")
ax_month.set_xticks(range(len(monthly)))
ax_month.set_xticklabels([d.strftime("%b") for d in monthly.index])
ax_month.set_title("Monthly Revenue", fontsize=11)
fig.suptitle("Sales Analytics Dashboard — H1 2026", fontsize=17, fontweight="bold")
plt.show()
Matplotlib gives complete control at the cost of verbosity. The next lesson, Seaborn, provides statistical charts in a fraction of the code.