Matplotlib — Python's
Core Visualisation Engine
Matplotlib is the foundation of Python plotting. Almost every other visualisation library (Seaborn, Pandas plotting, Plotly) is built on top of it. Understanding its Figure / Axes / Artist model unlocks complete control over every pixel.
Creating Basic Plots: Line, Bar, and Scatter Plots
Every Matplotlib plot lives inside a Figure (the window/canvas) containing one or more Axes (individual plot areas). Understanding this hierarchy is crucial before drawing any chart.
fig = plt.figure() creates the canvas. ax = fig.add_subplot() or fig, ax = plt.subplots() creates the plot area. All drawing commands (ax.plot(), ax.bar(), etc.) attach Artist objects to the Axes.
import matplotlib.pyplot as plt
import numpy as np
# ── Line Plot ─────────────────────────────────────────────
x = np.linspace(0, 2 * np.pi, 100)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, np.sin(x), label='sin(x)', color='#2563eb', linewidth=2)
ax.plot(x, np.cos(x), label='cos(x)', color='#dc2626', linewidth=2,
linestyle='--')
ax.plot(x, np.sin(2*x), label='sin(2x)', color='#059669', linewidth=1.5,
linestyle=':', alpha=0.8)
ax.set_title('Trigonometric Functions', fontsize=14, fontweight='bold')
ax.set_xlabel('x (radians)')
ax.set_ylabel('Amplitude')
ax.legend()
ax.grid(True, alpha=0.3)
ax.axhline(0, color='black', linewidth=0.8) # zero line
plt.tight_layout()
plt.savefig('line_plot.png', dpi=150, bbox_inches='tight')
plt.show()
# ── Key line style options ────────────────────────────────
# linestyle: '-', '--', ':', '-.'
# marker: 'o', 's', '^', 'D', '*', '+'
# color: name, hex, rgb tuple, 'C0'-'C9' (cycle colors)
ax.plot(x[:20], np.sin(x[:20]),
marker='o', # circle markers
markersize=6,
markerfacecolor='white',
markeredgewidth=1.5)
import matplotlib.pyplot as plt
import numpy as np
# ── Bar Plot ──────────────────────────────────────────────
categories = ['Engineering', 'HR', 'Finance', 'Marketing', 'Sales']
values = [85000, 72000, 90000, 68000, 77000]
errors = [3000, 2500, 4000, 2000, 3500] # error bars
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Vertical bar
bars = axes[0].bar(categories, values, color='#2563eb', alpha=0.8,
yerr=errors, capsize=4, error_kw={'linewidth':1.5})
axes[0].set_title('Average Salary by Department', fontweight='bold')
axes[0].set_ylabel('Salary ($)')
axes[0].tick_params(axis='x', rotation=20)
# Add value labels on bars
for bar, val in zip(bars, values):
axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 500,
f'${val/1000:.0f}K', ha='center', va='bottom',
fontsize=9, fontweight='bold')
# Horizontal bar
axes[1].barh(categories, values, color=['#2563eb','#059669','#d97706',
'#dc2626','#7c3aed'], alpha=0.85)
axes[1].set_title('Horizontal Bar Chart', fontweight='bold')
axes[1].set_xlabel('Salary ($)')
plt.tight_layout()
plt.show()
# ── Scatter Plot ──────────────────────────────────────────
np.random.seed(42)
n = 150
study_hours = np.random.uniform(1, 8, n)
exam_scores = 50 + 7 * study_hours + np.random.randn(n) * 8
departments = np.random.choice(['Eng','HR','Finance'], n)
colors_map = {'Eng':'#2563eb', 'HR':'#059669', 'Finance':'#d97706'}
colors = [colors_map[d] for d in departments]
sizes = np.random.uniform(30, 200, n) # bubble size
fig, ax = plt.subplots(figsize=(8, 5))
scatter = ax.scatter(study_hours, exam_scores,
c=colors, s=sizes, alpha=0.65, edgecolors='white',
linewidths=0.5)
# Trend line
m, b = np.polyfit(study_hours, exam_scores, 1)
ax.plot(study_hours, m*study_hours + b, color='black',
linewidth=1.5, linestyle='--', alpha=0.7, label=f'Trend: y={m:.1f}x+{b:.0f}')
ax.set_xlabel('Study Hours'), ax.set_ylabel('Exam Score')
ax.set_title('Study Hours vs Exam Score', fontweight='bold')
ax.legend()
plt.tight_layout()
plt.show()
Customizing Plots: Titles, Labels, Legends, and Grids
A chart is only as good as its labels. Matplotlib exposes fine-grained control over every text element, color, line, marker, tick, spine, and annotation — enabling publication-quality output.
Figure → Axes → Title, X-axis label, Y-axis label, X-ticks, Y-ticks, Spines (borders), Legend, Grid, Artists (lines/bars/patches).
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np
fig, ax = plt.subplots(figsize=(9, 5))
x = np.linspace(0, 10, 80)
ax.plot(x, np.sin(x)*3+5, color='#2563eb', lw=2, label='Revenue')
ax.plot(x, np.cos(x)*2+4, color='#dc2626', lw=2, ls='--', label='Cost')
ax.fill_between(x, np.sin(x)*3+5, np.cos(x)*2+4, alpha=0.12, color='green')
# ── Titles & Labels ───────────────────────────────────────
ax.set_title('Monthly Revenue vs Cost', fontsize=16, fontweight='bold',
pad=14)
ax.set_xlabel('Month', fontsize=13, labelpad=8)
ax.set_ylabel('Amount ($K)', fontsize=13, labelpad=8)
# ── Axis limits & ticks ───────────────────────────────────
ax.set_xlim(0, 10)
ax.set_ylim(0, 12)
ax.set_xticks(range(0, 11))
ax.set_xticklabels(['Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov'], rotation=30)
ax.yaxis.set_major_formatter(ticker.FormatStrFormatter('$%dK'))
# ── Grid ─────────────────────────────────────────────────
ax.grid(True, which='major', linestyle='--', linewidth=0.6, alpha=0.5)
ax.grid(True, which='minor', linestyle=':', linewidth=0.4, alpha=0.3)
ax.minorticks_on()
# ── Spines (borders) ─────────────────────────────────────
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_linewidth(0.8)
ax.spines['bottom'].set_linewidth(0.8)
# ── Legend ────────────────────────────────────────────────
ax.legend(loc='upper right', fontsize=11, framealpha=0.9,
edgecolor='#e2e8f0', shadow=True)
# ── Annotations ───────────────────────────────────────────
ax.annotate('Peak Revenue',
xy=(1.5, 7.9), xytext=(3, 10),
arrowprops=dict(arrowstyle='->', color='#2563eb', lw=1.5),
fontsize=10, color='#2563eb', fontweight='bold')
# ── Text box ─────────────────────────────────────────────
ax.text(0.02, 0.97, 'FY 2024', transform=ax.transAxes,
fontsize=10, color='gray', va='top',
bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.7))
# ── Color & style ─────────────────────────────────────────
ax.tick_params(axis='both', labelsize=10, colors='#475569')
fig.patch.set_facecolor('#fafafa') # figure background
ax.set_facecolor('#ffffff') # axes background
plt.tight_layout()
plt.show()
import matplotlib.pyplot as plt
# List available styles
print(plt.style.available)
# Apply a style
plt.style.use('seaborn-v0_8-whitegrid') # clean with grid
plt.style.use('ggplot') # R ggplot look
plt.style.use('bmh') # Bayesian Methods for Hackers
plt.style.use('fivethirtyeight') # 538 journalism style
plt.style.use('dark_background') # dark theme
# Temporary style (doesn't change global settings)
with plt.style.context('seaborn-v0_8-whitegrid'):
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [4, 5, 6])
plt.show()
# Custom rcParams (fine-grained control)
plt.rcParams.update({
'font.family': 'DejaVu Sans',
'font.size': 11,
'axes.linewidth': 0.8,
'axes.titlesize': 13,
'figure.dpi': 120,
'figure.facecolor': 'white',
})
Subplots and Layouts in Matplotlib
Subplots let you place multiple plots on one figure — essential for dashboards, comparison charts, and multi-panel analysis. Matplotlib offers three approaches: plt.subplots(), GridSpec, and add_axes() for precise positioning.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
x = np.linspace(0, 2*np.pi, 100)
# ── Simple 2×2 grid ───────────────────────────────────────
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
fig.suptitle('2×2 Subplot Grid', fontsize=14, fontweight='bold')
axes[0,0].plot(x, np.sin(x), color='#2563eb')
axes[0,0].set_title('sin(x)')
axes[0,1].plot(x, np.cos(x), color='#dc2626')
axes[0,1].set_title('cos(x)')
axes[1,0].plot(x, np.tan(np.clip(x, -1.4, 1.4)), color='#059669')
axes[1,0].set_title('tan(x) (clipped)')
axes[1,1].plot(x, np.sin(x)**2, color='#d97706')
axes[1,1].set_title('sin²(x)')
plt.tight_layout()
plt.show()
# ── GridSpec — unequal sizes ──────────────────────────────
fig = plt.figure(figsize=(10, 6))
gs = gridspec.GridSpec(2, 3, figure=fig,
height_ratios=[2, 1], # row 0 is 2× taller
width_ratios=[2, 1, 1], # col 0 is 2× wider
hspace=0.35, wspace=0.3)
ax_main = fig.add_subplot(gs[0, 0:2]) # spans col 0-1, row 0
ax_side = fig.add_subplot(gs[0, 2]) # col 2, row 0
ax_bot1 = fig.add_subplot(gs[1, 0])
ax_bot2 = fig.add_subplot(gs[1, 1])
ax_bot3 = fig.add_subplot(gs[1, 2])
ax_main.plot(x, np.sin(x), lw=2, label='Main Plot')
ax_main.set_title('Main (wide)', fontsize=11)
ax_side.bar(['A','B','C'], [3,7,5], color=['#2563eb','#059669','#d97706'])
ax_side.set_title('Side', fontsize=10)
for ax in [ax_bot1, ax_bot2, ax_bot3]:
ax.plot(x[:50], np.random.rand(50), alpha=.7)
plt.suptitle('GridSpec Layout', fontsize=13, fontweight='bold')
plt.show()
# ── Shared axes (perfect for comparisons) ─────────────────
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6), sharex=True)
ax1.plot(x, np.sin(x), color='#2563eb')
ax1.set_title('Shared X-axis — Zoom applies to both')
ax2.plot(x, np.cos(x), color='#dc2626')
plt.tight_layout()
plt.show()
# ── Inset axes (magnifying glass effect) ─────────────────
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(x, np.sin(x))
# Inset: zoom in on [0, 1]
ax_inset = ax.inset_axes([0.6, 0.5, 0.35, 0.35]) # [x,y,w,h] in axes coords
ax_inset.plot(x[x<=1], np.sin(x[x<=1]), color='#dc2626')
ax_inset.set_xlim(0, 1); ax_inset.set_title('Zoom', fontsize=8)
ax.indicate_inset_zoom(ax_inset, edgecolor='red')
plt.show()
Plotting Statistical Data: Histograms, Boxplots, and Pie Charts
Statistical charts reveal the shape and spread of data — things a simple mean cannot show. Histograms, boxplots, violin plots, and pie charts are the workhorse tools of exploratory data analysis (EDA).
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
data1 = np.random.normal(loc=65, scale=12, size=400) # Group A
data2 = np.random.normal(loc=78, scale=8, size=400) # Group B
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# ── Basic Histogram ────────────────────────────────────────
axes[0].hist(data1, bins=30, color='#2563eb', alpha=0.7, edgecolor='white',
label='Group A')
axes[0].hist(data2, bins=30, color='#dc2626', alpha=0.7, edgecolor='white',
label='Group B')
axes[0].axvline(data1.mean(), color='#2563eb', lw=2, ls='--',
label=f'Mean A: {data1.mean():.1f}')
axes[0].axvline(data2.mean(), color='#dc2626', lw=2, ls='--',
label=f'Mean B: {data2.mean():.1f}')
axes[0].set_title('Overlapping Histograms', fontweight='bold')
axes[0].set_xlabel('Score'); axes[0].set_ylabel('Frequency')
axes[0].legend()
# ── Density (KDE-like using density=True) ─────────────────
axes[1].hist(data1, bins=25, density=True, color='#2563eb', alpha=0.6,
label='Group A')
axes[1].hist(data2, bins=25, density=True, color='#dc2626', alpha=0.6,
label='Group B')
# Overlay smooth PDF approximation
from scipy.stats import norm # requires scipy
for data, col in [(data1,'#2563eb'),(data2,'#dc2626')]:
mu, std = data.mean(), data.std()
xr = np.linspace(data.min(), data.max(), 200)
axes[1].plot(xr, norm.pdf(xr, mu, std), color=col, lw=2.5)
axes[1].set_title('Density Histogram + PDF', fontweight='bold')
axes[1].set_xlabel('Score'); axes[1].set_ylabel('Density')
axes[1].legend()
plt.tight_layout()
plt.show()
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(0)
data = {
'Engineering': np.random.normal(85000, 12000, 80),
'HR': np.random.normal(65000, 8000, 60),
'Finance': np.random.normal(92000, 15000, 70),
'Marketing': np.random.normal(72000, 10000, 65),
}
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
# ── Boxplot ────────────────────────────────────────────────
bp = axes[0].boxplot(list(data.values()),
labels=list(data.keys()),
patch_artist=True, # fill boxes with color
medianprops={'linewidth':2, 'color':'white'},
flierprops={'marker':'o','markersize':4,'alpha':.5})
colors = ['#2563eb','#059669','#d97706','#dc2626']
for patch, color in zip(bp['boxes'], colors):
patch.set_facecolor(color); patch.set_alpha(0.75)
axes[0].set_title('Salary Boxplot by Dept', fontweight='bold')
axes[0].set_ylabel('Salary ($)')
axes[0].tick_params(axis='x', rotation=20)
axes[0].yaxis.set_major_formatter(plt.FuncFormatter(
lambda x, _: f'${int(x/1000)}K'))
# ── Violin Plot ───────────────────────────────────────────
parts = axes[1].violinplot(list(data.values()), showmedians=True,
showmeans=False)
for pc, col in zip(parts['bodies'], colors):
pc.set_facecolor(col); pc.set_alpha(0.7)
axes[1].set_xticks(range(1, len(data)+1))
axes[1].set_xticklabels(list(data.keys()), rotation=20)
axes[1].set_title('Salary Violin Plot', fontweight='bold')
axes[1].set_ylabel('Salary ($)')
axes[1].yaxis.set_major_formatter(plt.FuncFormatter(
lambda x, _: f'${int(x/1000)}K'))
# ── Pie / Donut Chart ─────────────────────────────────────
sizes = [len(v) for v in data.values()]
explode = (0.05, 0, 0, 0) # pull out first slice
wedges, texts, autotexts = axes[2].pie(
sizes, explode=explode, labels=list(data.keys()),
colors=colors, autopct='%1.1f%%', shadow=True, startangle=90,
wedgeprops={'edgecolor':'white', 'linewidth':2}
)
for at in autotexts: at.set_fontsize(9); at.set_color('white')
# Make it a donut
centre_circle = plt.Circle((0,0), 0.50, fc='white')
axes[2].add_patch(centre_circle)
axes[2].set_title('Headcount by Dept\n(Donut)', fontweight='bold')
plt.tight_layout()
plt.show()