python4 min read

Matplotlib Tutorial: Learn Data Visualization from Scratch (2026)

Matplotlib Tutorial: Learn Data Visualization from Scratch (2026)

Published:  |  Category: Python  |  Reading time: ~15 min
Matplotlib Tutorial: Learn Data Visualization from Scratch (2026)

My first Matplotlib plot was an ugly line chart with default colors, overlapping labels, and a gray background that made it look like 1980s terminal output. I've since learned that Matplotlib is not opinionated — it gives you a canvas and a vast API; you have to bring the design sense. Once you master its object-oriented interface (as opposed to pyplot's stateful one), you can create publication-quality figures with precise control over every element.

We'll progress from basic line plots to multi-panel figures with shared axes, custom styles, and annotations. The focus is on the object-oriented API (fig, ax) because that's what scales to complex layouts. By the end you'll be able to produce charts that communicate data clearly and look professional.

The Object-Oriented API: Figure and Axes

The two-level hierarchy is central: Figure is the top-level container (the whole window or saved image), and Axes is the actual plot area (where data is drawn). Multiple Axes can live in one Figure (subplots). I always use plt.subplots() which returns (fig, ax). You draw on ax — calling ax.plot(), ax.scatter(), ax.set_title() — and save the whole figure with fig.savefig().

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.sin(x)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y, label='sin(x)', color='steelblue', linewidth=2)
ax.set_title('Sine Wave', fontsize=14)
ax.set_xlabel('x')
ax.set_ylabel('sin(x)')
ax.legend()
fig.tight_layout()
fig.savefig('sine.png', dpi=150)

Multiple Subplots with Shared Axes

When comparing related data, subplots are more effective than overlaying everything in one chart. The subplots function accepts nrows and ncols, and sharex/sharey syncs axis limits across panels. I use .squeeze() or indexing to access individual axes from the returned array.

fig, axes = plt.subplots(2, 2, figsize=(10, 8), sharex='col', sharey='row')

x = np.linspace(0, 2*np.pi, 100)

axes[0, 0].plot(x, np.sin(x), color='crimson')
axes[0, 0].set_title('sin')
axes[0, 1].plot(x, np.cos(x), color='teal')
axes[0, 1].set_title('cos')
axes[1, 0].plot(x, np.tan(x), color='orange')
axes[1, 0].set_ylim(-5, 5)
axes[1, 0].set_title('tan')
axes[1, 1].plot(x, np.sin(2*x), color='purple')
axes[1, 1].set_title('sin(2x)')

fig.tight_layout()

Bar Charts, Histograms, and Categorical Data

For categorical comparisons, bar charts are the standard choice. ax.bar() positions bars at x-coordinates with custom widths and colors. Histograms (ax.hist()) bin continuous data into intervals — the bins parameter controls granularity, and density=True normalizes to a probability density. I always add edgecolor to bars for visual separation.

categories = ['Apples', 'Bananas', 'Cherries', 'Dates']
values = [42, 31, 18, 25]
colors = ['#e74c3c', '#f1c40f', '#c0392b', '#8e44ad']

fig, ax = plt.subplots()
bars = ax.bar(categories, values, color=colors, edgecolor='white', linewidth=1.5)
ax.bar_label(bars, padding=3)
ax.set_ylabel('Sales (tons)')
ax.set_title('Fruit Sales')

# Histogram of normally distributed data
data = np.random.randn(1000)
fig, ax = plt.subplots()
ax.hist(data, bins=30, density=True, alpha=0.7, color='steelblue')
ax.set_xlabel('Value')
ax.set_ylabel('Density')

Scatter Plots and Colormaps

Scatter plots show relationships between two continuous variables. The ax.scatter() function accepts c for color mapping and s for marker sizing. Using colormaps (cmap) to encode a third variable adds information density without clutter. I prefer perceptually uniform colormaps like viridis or magma over the default jet.

n = 200
x = np.random.randn(n)
y = x * 0.5 + np.random.randn(n) * 0.3
colors = x + y  # third dimension
sizes = np.abs(x) * 100 + 20

fig, ax = plt.subplots()
sc = ax.scatter(x, y, c=colors, s=sizes, cmap='viridis', alpha=0.8, edgecolors='w')
fig.colorbar(sc, ax=ax, label='Combined magnitude')
ax.set_xlabel('Feature A')
ax.set_ylabel('Feature B')
ax.set_title('Scatter with Color and Size Encoding')

Customizing Styles and Themes

Matplotlib supports style sheets that set defaults for colors, fonts, grid lines, and figure size. I use plt.style.use('seaborn-v0_8') for clean defaults, or create a custom style for consistency across a report. rcParams lets you fine-tune individual settings. Font sizes, tick parameters, and spines (the plot borders) are common customizations to make charts look less cluttered.

print(plt.style.available)  # List all styles
plt.style.use('seaborn-v0_8-whitegrid')

# Custom rcParams
plt.rcParams.update({
    'figure.dpi': 120,
    'font.size': 12,
    'axes.spines.top': False,
    'axes.spines.right': False,
})

# Now all plots use these settings
fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_title('Clean Style')

Annotations, Text, and Saving Figures

Annotations draw attention to specific data points. ax.annotate() places text with an optional arrow pointing to coordinates. For more control, ax.text() positions text anywhere in data or axes coordinates. When saving, fig.savefig() supports raster (png, jpg) and vector (pdf, svg) formats. Set dpi for resolution and bbox_inches='tight' to crop whitespace.

fig, ax = plt.subplots()
ax.plot(x, y)

# Highlight the peak
peak_x = x[np.argmax(y)]
peak_y = np.max(y)
ax.scatter([peak_x], [peak_y], color='red', s=100, zorder=5)
ax.annotate(
    f'Peak: {peak_y:.2f}',
    xy=(peak_x, peak_y),
    xytext=(peak_x + 0.5, peak_y + 0.3),
    arrowprops=dict(arrowstyle='->', color='black'),
    fontsize=11
)

fig.savefig('annotated_plot.pdf', bbox_inches='tight', dpi=150)

Frequently Asked Questions

Should I use pyplot or the object-oriented API?

Use the OO API (fig, ax) for anything beyond exploratory one-liners. The pyplot state machine (plt.plot, plt.xlabel) is convenient for quick scripts but causes bugs in complex layouts or when embedding in GUIs.

How do I fix overlapping labels?

Call fig.tight_layout() before saving. For x-axis tick labels, use plt.setp(ax.get_xticklabels(), rotation=45, ha='right'). For legends, place them outside the plot with bbox_to_anchor=(1.05, 1).

Can Matplotlib handle interactive plots?

Matplotlib supports interactive backends (%matplotlib notebook in Jupyter). For fully interactive web visualizations, use Plotly, Bokeh, or a JavaScript library. Matplotlib is designed for static publication figures.

How do I set the figure size after creation?

Set figsize in plt.subplots(figsize=(w, h)). To change an existing figure, use fig.set_size_inches(w, h) and then fig.tight_layout() to adjust.

Originally published on Ayodhyyya. Last updated June 1, 2026.