Skip to content

Matplotlib Cheatsheet

Comprehensive Python plotting library for data visualization.

01

Getting Started

Installation & First Plot

Matplotlib is the foundation of Python plotting. pyplot is the state-based interface (like MATLAB). Always call plt.show() to render the figure in a script; in Jupyter notebooks the figure renders automatically when a cell ends.

matplotlib
# install
pip install matplotlib

import matplotlib.pyplot as plt
import numpy as np

# a simple line plot
x = np.linspace(0, 10, 100)
y = np.sin(x)

plt.plot(x, y)
plt.title("Sine Wave")
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.show()

pyplot State Interface

The pyplot interface mirrors MATLAB: each call operates on the 'current' figure/axes tracked internally. It is concise for quick plots but can get confusing with multiple subplots. Use plt.gcf()/plt.gca() to access the current figure/axes objects.

matplotlib
import matplotlib.pyplot as plt

# pyplot tracks the "current" figure and axes
plt.figure()             # create a new figure
plt.plot([1, 2, 3], [4, 5, 2])
plt.title("State-based")  # applies to the current axes
plt.xlim(0, 4)
plt.ylim(0, 6)
plt.show()

# plt.gcf() -> current figure, plt.gca() -> current axes
print(plt.gca())

Figure & Axes (OO Interface)

The Object-Oriented (OO) interface is the recommended approach for anything beyond a single plot. You create a Figure (the whole canvas) and one or more Axes (individual plots) and call methods on them. It scales cleanly to subplots, twin axes and complex layouts.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

# object-oriented interface: explicit Figure and Axes objects
fig, ax = plt.subplots()      # fig is the canvas, ax is a single Axes
x = np.linspace(0, 5, 50)
ax.plot(x, x**2, label="x^2")
ax.plot(x, x**3, label="x^3")
ax.set_title("OO Interface")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.legend()
plt.show()

Common Imports & Conventions

plt is the near-universal alias for pyplot and np for NumPy — almost every tutorial and library expects these. rcParams is the global configuration dictionary: set it once at the top of a script to fix DPI, font size, figure size and many other defaults.

matplotlib
# the universal alias
import matplotlib.pyplot as plt
import numpy as np

# for the OO interface you often see
from matplotlib.figure import Figure
from matplotlib.axes import Axes

# set a global style once
plt.rcParams["figure.dpi"] = 100
plt.rcParams["font.size"] = 11

# inline backend in Jupyter (not needed in scripts)
# %matplotlib inline

Backends & plt.show()

A backend renders the figure: interactive backends (TkAgg, Qt5Agg) open a window, non-interactive ones (Agg) only write files. Call matplotlib.use() before importing pyplot. plt.show() blocks in scripts until windows close; in Jupyter, %matplotlib inline embeds figures in the notebook.

matplotlib
import matplotlib

# list available backends
print(matplotlib.rcsetup.all_backends)

# force a backend (do this BEFORE importing pyplot)
# matplotlib.use("Agg")        # non-interactive, for saving files
# matplotlib.use("TkAgg")      # interactive Tk window

import matplotlib.pyplot as plt
plt.plot([1, 2, 3])
# plt.show() blocks until the window is closed
plt.savefig("out.png")  # saving works without any GUI backend
02

Line Plots (plot)

Basic Line Plot

plt.plot(x, y) draws a line connecting the (x, y) points in order. If you pass a single array it is used as y and x defaults to 0..N-1. plt.grid(True) toggles gridlines. Points are connected in array order, so for a smooth curve sort x first.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(0, 10, 0.5)
y = x * 2

plt.plot(x, y)
plt.title("y = 2x")
plt.xlabel("x")
plt.ylabel("y")
plt.grid(True)
plt.show()

Multiple Lines

Each plt.plot() call adds a new line to the current axes and auto-advances the color cycle. Pass a label= to each so plt.legend() can identify them. Matplotlib handles axis limits automatically, expanding to fit the largest range.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 100)
plt.plot(x, np.sin(x), label="sin")
plt.plot(x, np.cos(x), label="cos")
plt.plot(x, np.sin(x) + np.cos(x), label="sin+cos")
plt.legend()
plt.title("Trig Functions")
plt.show()

Line Styles & Markers

The format string 'color+marker+linestyle' (e.g. 'ro--') is compact but cryptic. Prefer explicit color=, marker=, linestyle= keyword arguments for readable code. Common linestyles: '-' '--' ':' '-.'. Common markers: 'o' 's' '^' 'D' 'v' '+' 'x'.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 5, 20)
# format string: [color][marker][linestyle]
plt.plot(x, x, "r-")        # red solid line
plt.plot(x, x*2, "go--")    # green circles, dashed
plt.plot(x, x*3, "bs:")     # blue squares, dotted
plt.plot(x, x*4, "k^-.")    # black triangles, dash-dot

# explicit kwargs (clearer, recommended)
plt.plot(x, x*5, color="purple", marker="D",
         linestyle="-.", linewidth=2, markersize=6)

Colors

Matplotlib accepts many color formats: names ('red'), hex strings ('#FF8800'), RGB(A) tuples, Tableau names ('tab:blue'), cycle references ('C0'..'C9') and grayscale strings ('0.5'). 'C0' picks the current style's first color, keeping plots consistent with the color cycle.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 1, 30)
plt.plot(x, x, color="red")              # named color
plt.plot(x, x+0.2, color="#FF8800")      # hex RGB
plt.plot(x, x+0.4, color=(0.1, 0.8, 0.2))# RGB tuple 0-1
plt.plot(x, x+0.6, color="C0")           # cycle color 0
plt.plot(x, x+0.8, color="tab:purple")   # Tableau palette
# gray shorthand: a string of float in [0,1]
plt.plot(x, x+1.0, color="0.5")          # mid gray
plt.show()

Line Width, Alpha & zorder

linewidth controls stroke thickness (default 1.5). alpha sets transparency and is essential when curves overlap so you can see what is beneath. zorder controls stacking — higher values are drawn later (on top). Default zorder is 2 for lines, 1 for patches.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
# alpha = transparency (0 invisible, 1 opaque)
# zorder = draw order; higher draws on top
plt.fill_between(x, np.sin(x), alpha=0.3, zorder=1)
plt.plot(x, np.sin(x), linewidth=2, zorder=2)
plt.plot(x, np.cos(x), linewidth=4, alpha=0.7, zorder=3)
plt.show()

Step & Stairs Plots

plt.step() draws step functions useful for digital signals and discrete data; where controls where the step rises ('pre', 'post', 'mid'). plt.stairs() (3.4+) is the modern equivalent that takes y values and separate bin edges, and integrates well with hist-like data.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(5)
y = [1, 3, 2, 4, 3]

# step: like plot but with horizontal/vertical steps
plt.step(x, y, where="post", label="post")
plt.step(x, y + 1, where="mid", label="mid")
plt.step(x, y + 2, where="pre", label="pre")
plt.legend(title="where=")

# stairs (matplotlib 3.4+): step from explicit bin edges
plt.stairs([1, 2, 1], baseline=0)

plt.show()
03

Scatter Plots

Basic Scatter

plt.scatter(x, y) plots points without connecting lines. It is slower than plt.plot(x, y, 'o') when you have tens of thousands of points because scatter lets each point have its own size and color. Use plt.plot with a marker for max speed with uniform styling.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
x = rng.random(50)
y = rng.random(50)

plt.scatter(x, y)
plt.title("Basic Scatter")
plt.xlabel("x")
plt.ylabel("y")
plt.axis("equal")   # equal aspect ratio
plt.show()

Marker Size & Color

The s argument is marker AREA in points squared (not radius), so a 4x larger s is only 2x the radius. The c (color) argument with cmap maps a numeric array onto a colormap. Use alpha < 1 when points overlap so density is visible.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(1)
n = 50
x = rng.random(n)
y = rng.random(n)
sizes = rng.random(n) * 500     # area in points^2
colors = rng.random(n)          # mapped to colormap

plt.scatter(x, y, s=sizes, c=colors, cmap="viridis", alpha=0.6)
plt.colorbar(label="value")
plt.title("Bubble Chart")
plt.show()

Colormap Scatter

Passing a 1-D array to c maps each point to a colormap; plt.colorbar(sc) adds a colorbar (you must pass the scatter's return value so the colorbar knows the mapping). vmin/vmax fix the colormap range; without them matplotlib autoscales to the data min/max.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 200)
y = np.sin(x)
c = x           # color by x value

sc = plt.scatter(x, y, c=c, cmap="plasma", vmin=0, vmax=10)
cb = plt.colorbar(sc)
cb.set_label("x position")
plt.title("Colored by x")
plt.show()

Transparency & Overplotting

With many overlapping points a solid scatter hides density. Drop alpha to ~0.05 and shrink the marker to reveal structure. For very large data (100k+ points), plt.hexbin or plt.hist2d aggregate into bins and are both faster and clearer than raw scatter.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(2)
n = 5000
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)

# dense scatter: low alpha reveals density
plt.scatter(x, y, s=5, alpha=0.05, c="navy")
plt.title("Density via alpha")

# alternative: hexbin for very large datasets
plt.figure()
plt.hexbin(x, y, gridsize=40, cmap="Blues")
plt.colorbar(label="count")
plt.show()

Categorical Scatter

Matplotlib has no built-in strip plot, so scatter with manual jitter on the x-axis is the standard DIY approach. For publication-quality categorical plots (box, violin, swarm) prefer Seaborn, which is built on Matplotlib and integrates with plt styling.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(3)
groups = ["A", "B", "C"]
data = [rng.normal(0, 1, 30),
        rng.normal(2, 1, 30),
        rng.normal(1, 1.5, 30)]

for i, d in enumerate(data):
    # add jitter on x so points do not stack on one line
    jitter = rng.uniform(-0.1, 0.1, len(d))
    plt.scatter(np.full(len(d), i) + jitter, d, label=groups[i])

plt.xticks(range(len(groups)), groups)
plt.legend()
plt.show()
04

Bar Charts

Vertical Bar Chart

plt.bar(x, height) draws vertical bars; x may be strings (categorical). The returned BarContainer lets you annotate or recolor individual bars. The loop with plt.text() labels each bar — adjust ha/va ('center'/'bottom') and the y offset so labels sit just above the bar.

matplotlib
import matplotlib.pyplot as plt

categories = ["Apple", "Banana", "Cherry", "Date"]
values = [25, 40, 30, 15]

bars = plt.bar(categories, values, color="tab:blue", edgecolor="black")

# annotate each bar with its value
for bar, v in zip(bars, values):
    plt.text(bar.get_x() + bar.get_width()/2, v + 1,
             str(v), ha="center", va="bottom")

plt.title("Fruit Sales")
plt.ylabel("units sold")
plt.ylim(0, 50)
plt.show()

Horizontal Bar Chart

plt.barh(y, width) draws horizontal bars — ideal for long category labels that would overlap on a vertical chart. Call ax.invert_yaxis() so the first item appears at the top, matching how people read lists. Width defaults to 0.8.

matplotlib
import matplotlib.pyplot as plt

items = ["Reading", "Coding", "Sleep", "Exercise"]
hours = [2, 6, 8, 1]

plt.barh(items, hours, color="tab:green")
plt.xlabel("hours per day")
plt.title("Daily Activities")

# invert so the first item appears at the top
plt.gca().invert_yaxis()
plt.show()

Grouped Bar Chart

Grouped bars place two (or more) series side by side by offsetting the x positions by half the bar width. The pattern x +/- w/2 with width w places them flush. Use np.arange for numeric x positions so the math works, then relabel with plt.xticks.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

labels = ["Q1", "Q2", "Q3", "Q4"]
a = [20, 35, 30, 35]
b = [25, 32, 34, 20]
x = np.arange(len(labels))
w = 0.35

plt.bar(x - w/2, a, w, label="2023")
plt.bar(x + w/2, b, w, label="2024")
plt.xticks(x, labels)
plt.ylabel("revenue")
plt.legend()
plt.show()

Stacked Bar Chart

Stacked bars use the bottom= argument: each series starts where the previous one ended. For more than two series, accumulate: bottom = [m+w for m, w in zip(men, women)]. Stacked bars show totals and composition, but make the upper series harder to compare across categories.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

labels = ["A", "B", "C"]
men = [20, 35, 30]
women = [25, 32, 34]

plt.bar(labels, men, label="Men")
plt.bar(labels, women, bottom=men, label="Women")
plt.legend()
plt.title("Stacked Bars")
plt.show()

Bar with Error Bars

The yerr argument adds vertical error bars; capsize sets the length of the end caps. Pass error_kw to style the bars. For asymmetric errors pass yerr=[lower_errors, upper_errors]. This works on barh (xerr) and on plt.errorbar too.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

labels = ["A", "B", "C"]
means = [20, 35, 30]
errors = [2, 4, 3]

plt.bar(labels, means, yerr=errors, capsize=6,
        color="tab:orange", edgecolor="black",
        error_kw={"ecolor": "black", "linewidth": 1.5})
plt.title("Bar with Error Bars")
plt.ylabel("mean value")
plt.show()
05

Histograms

Basic Histogram

plt.hist(x) bins the data and draws bars. bins can be an integer (number of bins) or a sequence of explicit edges. By default it returns counts, so the y-axis is frequency. The return value is (counts, bin_edges, patches) which you can use for further customization.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
data = rng.normal(0, 1, 1000)

plt.hist(data, bins=30)
plt.title("Histogram")
plt.xlabel("value")
plt.ylabel("frequency")
plt.show()

Choosing Bins

Bin choice drastically changes the look of a histogram. 'auto' uses the maximum of Sturges and Freedman-Diaconis rules — a sensible default. Pass an explicit edge array for full control and comparable bins across datasets. Too few bins hides structure; too many add noise.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(1)
data = rng.normal(0, 1, 1000)

# auto bin selection (Freedman-Diaconis, Sturges, etc.)
plt.hist(data, bins="auto", alpha=0.5, label="auto")
plt.hist(data, bins=10, alpha=0.5, label="10 bins")
plt.hist(data, bins=np.arange(-4, 4, 0.25),
         alpha=0.5, label="0.25 width")
plt.legend()
plt.show()

Multiple Histograms

Overlapping histograms need alpha < 1 to see both distributions; use distinct colors. For more than two groups, stacked=True avoids clutter but makes comparison harder. Consider kdeplot (Seaborn) for smooth density curves that overlay more cleanly than histograms.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(2)
a = rng.normal(0, 1, 1000)
b = rng.normal(1, 1.2, 1000)

plt.hist(a, bins=30, alpha=0.6, label="A", color="tab:blue")
plt.hist(b, bins=30, alpha=0.6, label="B", color="tab:orange")
plt.legend()
plt.show()

# stacked alternative
plt.figure()
plt.hist([a, b], bins=30, stacked=True, label=["A", "B"])
plt.legend()
plt.show()

Density / Normalized

density=True divides counts by (total * bin_width) so the bar areas sum to 1, producing a probability density. This makes histograms comparable across different sample sizes and lets you overlay a theoretical PDF. Use weights= to weight individual samples.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(3)
data = rng.normal(0, 1, 1000)

# density=True normalizes the area to 1 (PDF)
plt.hist(data, bins=30, density=True, alpha=0.6)

# overlay the theoretical PDF
from scipy.stats import norm
xs = np.linspace(-4, 4, 200)
plt.plot(xs, norm.pdf(xs), "r-", lw=2)
plt.title("Normalized Histogram + PDF")
plt.show()

2D Histogram

plt.hist2d bins two variables into a 2-D grid and colors each cell by count — the discrete cousin of a scatter for dense data. For smoother output use plt.hexbin (hexagonal bins). Both accept a cmap and return values you can pass to colorbar.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(4)
x = rng.normal(0, 1, 10000)
y = x + rng.normal(0, 1, 10000)

plt.hist2d(x, y, bins=40, cmap="Blues")
plt.colorbar(label="count")
plt.title("2D Histogram")
plt.xlabel("x")
plt.ylabel("y")
plt.show()
06

Pie Charts

Basic Pie Chart

plt.pie(x, labels) draws a pie from the values in x. autopct formats the percentage labels (a printf-style string or function). startangle rotates the start of the first wedge. Always plt.axis('equal') so the pie is a true circle rather than an ellipse.

matplotlib
import matplotlib.pyplot as plt

labels = ["Rent", "Food", "Transport", "Fun"]
sizes = [1200, 600, 300, 400]

plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=90)
plt.title("Monthly Budget")
plt.axis("equal")   # circle, not ellipse
plt.show()

Explode & Shadows

explode is a tuple of the same length as sizes; each value offsets that wedge outward from the center (0 = no offset). shadow=True adds a drop shadow for depth. Use explode sparingly to highlight one slice — over-exploding every wedge looks cluttered.

matplotlib
import matplotlib.pyplot as plt

labels = ["A", "B", "C", "D"]
sizes = [30, 25, 25, 20]
explode = (0, 0.1, 0, 0)   # pull the 2nd wedge out

plt.pie(sizes, labels=labels, explode=explode,
        shadow=True, autopct="%1.0f%%",
        startangle=140)
plt.title("Exploded Pie")
plt.show()

Donut Chart

A donut is just a pie with a hole: set wedgeprops={'width': 0.4} to draw rings instead of full wedges. The center hole can hold a summary label via plt.text(0, 0, ...). Donuts are generally easier to read than pies because the ring length encodes value better than wedge angle.

matplotlib
import matplotlib.pyplot as plt

labels = ["A", "B", "C"]
sizes = [40, 35, 25]

wedges, texts = plt.pie(sizes, labels=labels,
                        wedgeprops=dict(width=0.4, edgecolor="w"))
plt.title("Donut Chart")
# add center text
plt.text(0, 0, "Total\n100", ha="center", va="center", fontsize=14)
plt.show()

Percentage & Start Angle

autopct can be a function that receives the percentage and returns a string, letting you show both percent and absolute count. startangle + counterclock=False control wedge direction (start at top, go clockwise — the conventional reading order). pctdistance positions the percent labels.

matplotlib
import matplotlib.pyplot as plt

labels = ["North", "South", "East", "West"]
sizes = [32, 24, 28, 16]
colors = ["#4C72B0", "#DD8452", "#55A868", "#C44E52"]

plt.pie(sizes, labels=labels, colors=colors,
        autopct=lambda p: f"{p:.1f}%\n({p*sum(sizes)/100:.0f})",
        startangle=90, counterclock=False,
        pctdistance=0.75)
plt.show()

Nested Pie (Concentric)

Two plt.pie calls with different radius and width values create concentric rings, a simple 'sunburst'. The outer ring typically shows top-level categories and the inner ring their breakdowns. Keep the inner radius smaller and add edgecolor='w' to separate wedges visually.

matplotlib
import matplotlib.pyplot as plt

# outer ring: regions, inner ring: sub-categories
outer = [40, 30, 30]
inner = [20, 20, 15, 15, 25, 5]
outer_labels = ["A", "B", "C"]
inner_labels = ["A1", "A2", "B1", "B2", "C1", "C2"]

plt.pie(outer, labels=outer_labels, radius=1.0,
        wedgeprops=dict(width=0.3, edgecolor="w"))
plt.pie(inner, labels=inner_labels, radius=0.7,
        wedgeprops=dict(width=0.3, edgecolor="w"))
plt.title("Nested Pie")
plt.show()
07

Subplots & Layouts

plt.subplot

plt.subplot(rows, cols, index) selects the (1-indexed) axes in a grid — the legacy MATLAB-style way. The index runs left-to-right, top-to-bottom. It mutates the 'current axes', so subsequent plt calls draw there. Prefer plt.subplots() for new code; it is cleaner and returns real Axes objects.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 5, 50)

# subplot(nrows, ncols, index)  -- 1-indexed!
plt.subplot(2, 2, 1)
plt.plot(x, x)

plt.subplot(2, 2, 2)
plt.plot(x, x**2)

plt.subplot(2, 2, 3)
plt.plot(x, x**3)

plt.subplot(2, 2, 4)
plt.plot(x, np.sin(x))

plt.tight_layout()
plt.show()

plt.subplots

plt.subplots(nrows, ncols) returns (fig, axes) where axes is a NumPy array of Axes — index it like axes[row, col]. sharex/sharey align ticks and save space. fig.suptitle() sets a figure-level title above all subplots. Always plt.tight_layout() to avoid label overlap.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

# one figure, 2x2 grid of Axes, shared x and y
fig, axes = plt.subplots(2, 2, figsize=(8, 6),
                         sharex=True, sharey=True)

axes[0, 0].plot(x, np.sin(x)); axes[0, 0].set_title("sin")
axes[0, 1].plot(x, np.cos(x)); axes[0, 1].set_title("cos")
axes[1, 0].plot(x, np.tan(x)); axes[1, 0].set_title("tan")
axes[1, 1].plot(x, -np.sin(x)); axes[1, 1].set_title("-sin")

fig.suptitle("Trig Family")
plt.tight_layout()
plt.show()

GridSpec

GridSpec gives full control over subplot sizes — you can merge cells with slicing (gs[0, :] = whole row, gs[1:, 2] = tall right column). Use width_ratios/height_ratios to make columns/rows of unequal size. This is the standard tool for complex dashboards.

matplotlib
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(8, 6))
gs = gridspec.GridSpec(3, 3, figure=fig)

ax1 = fig.add_subplot(gs[0, :])     # full top row
ax2 = fig.add_subplot(gs[1, :2])    # middle, left 2 cols
ax3 = fig.add_subplot(gs[1:, 2])    # right, spans 2 rows
ax4 = fig.add_subplot(gs[2, 0:2])   # bottom-left

ax1.set_title("wide top")
ax3.set_title("tall right")
plt.tight_layout()
plt.show()

Sharing Axes

sharex=True (or sharey=True) links axes so panning/zooming one updates the others — great for stacked time series. It also hides redundant tick labels on inner axes. The shared groups are accessible via ax.get_shared_x_axes(); you can remove an axes from the group to decouple it.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)

ax1.plot(x, np.sin(x))
ax2.plot(x, np.cos(x))

# zooming on one now zooms both
ax1.set_xlim(0, 5)
# decouple later if needed
# ax2.get_shared_x_axes().remove(ax2)

fig.suptitle("Shared x-axis")
plt.show()

Tight Layout & Constrained

tight_layout() adjusts spacing once to prevent label overlap, but it does not know about fig.suptitle — pass rect to reserve space. layout='constrained' (or fig=Figure(layout='constrained')) is the modern alternative that recalculates on every draw, handles suptitle and colorbars correctly, and is recommended for new code.

matplotlib
import matplotlib.pyplot as plt

# tight_layout: simple, adjusts subplot spacing
fig, axes = plt.subplots(2, 2)
for ax in axes.flat:
    ax.set_xlabel("x label")
    ax.set_ylabel("y label")
    ax.set_title("a title")
fig.suptitle("tight_layout")
plt.tight_layout()
# leave room for suptitle
plt.tight_layout(rect=[0, 0, 1, 0.95])
plt.show()

# constrained_layout: recomputes on every draw (more robust)
fig, axes = plt.subplots(2, 2, layout="constrained")
fig.suptitle("constrained_layout")
plt.show()
08

Legends & Annotations

Basic Legend

plt.legend() collects labels from each plotted artist. loc sets the in-axes position ('upper right', 'lower left', 'best', ...). For an external legend, combine bbox_to_anchor=(x, y) (axes-fraction coords) with loc to anchor that corner of the legend box to that point.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 1, 30)
plt.plot(x, x, label="linear")
plt.plot(x, x**2, label="quadratic")
plt.plot(x, x**3, label="cubic")
plt.legend()
plt.show()

# legend outside the axes
plt.figure()
plt.plot(x, x, label="linear")
plt.plot(x, x**2, label="quadratic")
plt.legend(loc="upper left", bbox_to_anchor=(1, 1))
plt.tight_layout()
plt.show()

Legend Position & Columns

ncol lays out legend entries in multiple columns — useful when you have many short labels. frameon/framealpha/fancybox control the legend box appearance. title adds a heading. For a borderless look set frameon=False; this often looks cleaner in publications.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 1, 30)
for i in range(1, 6):
    plt.plot(x, x**i, label=f"x^{i}")

plt.legend(loc="upper center", ncol=3, fontsize=9,
           frameon=True, framealpha=0.9,
           title="Powers", title_fontsize=10)
plt.title("Custom Legend")
plt.show()

Annotate Points

ax.annotate(text, xy, xytext) places text at xytext and (optionally) draws an arrow to xy. xy is in data coordinates by default. arrowprops is a dict of arrow styling; arrowstyle='->' is the most common. annotate is far more flexible than plt.text for callouts.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
plt.plot(x, y)

# mark and label the maximum
idx = np.argmax(y)
plt.annotate(f"max = {y[idx]:.2f}",
             xy=(x[idx], y[idx]),        # point to annotate
             xytext=(x[idx]-1, 0.5),     # text position
             arrowprops=dict(arrowstyle="->"))
plt.show()

Annotate with Arrows

arrowprops styles the connector: arrowstyle ('->', '-|>', '<->'), color, lw. connectionstyle bends the arrow — 'arc3,rad=0.3' curves it, 'angle3' makes a right-angle elbow, 'bar' adds a T-bar. These let you build clear callouts that route around data without overlapping curves.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 100)
plt.plot(x, x**3 - 3*x)

plt.annotate("local min", xy=(1, -2), xytext=(2, -10),
             arrowprops=dict(arrowstyle="->", color="red",
                             lw=2, connectionstyle="arc3,rad=0.3"))
plt.annotate("local max", xy=(-1, 2), xytext=(-3, 8),
             arrowprops=dict(arrowstyle="-|>", color="blue",
                             connectionstyle="angle3"))
plt.show()

Custom Legend Handles

When the default legend (built from plotted artists) is not what you want, build your own handles with Line2D, Patch or Line2D-with-no-data and pass handles= to legend(). This is essential for legends that combine lines, patches and scatter markers in a single key.

matplotlib
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Patch

plt.plot([1, 2, 3], [1, 4, 9], "o-", color="tab:blue")
plt.plot([1, 2, 3], [1, 8, 27], "s--", color="tab:orange")

custom = [
    Line2D([0], [0], color="tab:blue", marker="o", label="squared"),
    Line2D([0], [0], color="tab:orange", marker="s", ls="--", label="cubed"),
    Patch(facecolor="gray", label="reference band"),
]
plt.legend(handles=custom)
plt.show()
09

Styles, Colors & rcParams

Built-in Styles

plt.style.use(name) applies a preset look globally; plt.style.context(name) applies it temporarily inside a with-block. Popular styles include 'ggplot', 'seaborn-v0_8-*', 'bmh', 'fivethirtyeight'. Note Seaborn styles were renamed with a '-v0_8' suffix in Matplotlib 3.6+.

matplotlib
import matplotlib.pyplot as plt

# list all available styles
print(plt.style.available)

# use a style globally
plt.style.use("seaborn-v0_8-whitegrid")

# or as a context manager (temporary)
with plt.style.context("dark_background"):
    plt.figure()
    plt.plot([1, 2, 3], [1, 4, 9])
    plt.title("dark")
    plt.show()

plt.plot([1, 2, 3], [9, 4, 1])
plt.title("default again")
plt.show()

Colormaps

Colormaps fall into three families: sequential (viridis, plasma) for ordered 0..N data, diverging (coolwarm, RdBu) for data with a meaningful midpoint (use with vmin/vmax centered on 0), and qualitative (Set2, tab10) for distinct categories. Prefer perceptually-uniform maps like viridis — they are colorblind-safe and print legibly.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

# sequential, diverging, qualitative
for cmap in ["viridis", "plasma", "coolwarm", "RdBu", "Set2"]:
    plt.scatter(x, y, c=x, cmap=cmap, label=cmap)
    plt.colorbar(label=cmap)
    plt.title(cmap)
    plt.show()

Custom Color Cycle

axes.prop_cycle (a cycler object) defines the sequence of properties cycled through as you add lines. You can cycle color, linestyle, marker and more — combine with cycler(color=[...]) + cycler(linestyle=[...]) to advance both simultaneously. This gives every plot a consistent, branded palette.

matplotlib
import matplotlib.pyplot as plt
from cycler import cycler

# set the default color cycle for new axes
plt.rcParams["axes.prop_cycle"] = cycler(
    color=["#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00"])

for i in range(1, 6):
    plt.plot([0, 1, 2], [i]*3, label=f"line {i}", lw=4)

plt.legend()
plt.title("Custom Cycle")
plt.show()

rcParams

rcParams is the master configuration dictionary controlling every default — figure size, DPI, fonts, line widths, grid, savefig options. Set values once at the top of a script for consistent output. plt.rcParams.update({...}) sets several at once. Use mpl.rcParamsFile or a matplotlibrc file to make defaults permanent.

matplotlib
import matplotlib.pyplot as plt
import matplotlib as mpl

# read / set individual parameters
mpl.rcParams["figure.figsize"] = (8, 4)
mpl.rcParams["figure.dpi"] = 100
mpl.rcParams["font.size"] = 12
mpl.rcParams["axes.grid"] = True
mpl.rcParams["axes.grid.which"] = "both"
mpl.rcParams["savefig.bbox"] = "tight"

# update many at once
plt.rcParams.update({
    "axes.titlesize": "large",
    "axes.labelsize": "medium",
    "lines.linewidth": 2,
})

plt.plot([1, 2, 3])
plt.title("Configured Defaults")
plt.show()

Color Formats

matplotlib.colors provides conversion (to_hex, to_rgb), named color lists, and colormap construction. LinearSegmentedColormap.from_list builds a continuous colormap from a few anchor colors. For categorical custom maps use ListedColormap(['#aaa', '#bbb', ...]). These let you match brand palettes exactly.

matplotlib
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors

# all named colors
print(list(mcolors.CSS4_COLORS)[:5])

# convert between formats
print(mcolors.to_hex((0.1, 0.2, 0.5)))      # #1a3380
print(mcolors.to_rgb("#ff8800"))            # (1.0, 0.533, 0.0)

# make a custom colormap from a list of colors
cmap = mcolors.LinearSegmentedColormap.from_list(
    "rg", ["red", "yellow", "green"])
import numpy as np
plt.imshow(np.linspace(0, 1, 256).reshape(1, -1),
           cmap=cmap, aspect="auto")
plt.title("Custom Colormap")
plt.show()
10

Axis Settings

Limits (xlim / ylim)

plt.xlim/plt.ylim (or ax.set_xlim/ax.set_ylim) set the visible data range. Passing None leaves a limit to autoscale, e.g. set_ylim(None, 5) only fixes the top. plt.margins() adds padding as a fraction of the data range instead of hard limits.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

# set explicit limits
plt.xlim(2, 8)
plt.ylim(-1.5, 1.5)

# or autoscale with margin
# plt.margins(x=0.05, y=0.1)

# query current limits
print(plt.xlim())   # (2.0, 8.0)
plt.show()

Ticks

plt.xticks(positions) sets where ticks appear; plt.xticks(positions, labels) also sets their text. Pass an empty list to hide ticks entirely. For numeric axes you usually let the MaxNLocator choose ticks automatically and only override for categorical or symbolic axes (like multiples of pi).

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

# set tick positions
plt.xticks([0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi])

# set positions AND labels
plt.xticks([0, np.pi/2, np.pi, 3*np.pi/2, 2*np.pi],
           ["0", "pi/2", "pi", "3pi/2", "2pi"])

# turn off ticks
# plt.xticks([])

plt.show()

Tick Formatting

The ticker module gives precise control: Locators decide WHERE ticks go (MultipleLocator every N units, MaxNLocator up to N ticks, LogLocator for log axes); Formatters decide the LABEL text (FormatStrFormatter printf-style, FuncFormatter arbitrary function). Use minor locators for fine gridlines.

matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MultipleLocator, FormatStrFormatter, FuncFormatter

x = np.linspace(0, 5, 100)
plt.plot(x, x**2)

ax = plt.gca()
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.xaxis.set_minor_locator(MultipleLocator(0.25))
ax.yaxis.set_major_formatter(FormatStrFormatter("%.0f"))

# format with a function
ax.yaxis.set_major_formatter(
    FuncFormatter(lambda v, p: f"{v/1000:.1f}k"))

plt.show()

Log Scale

plt.xscale('log') / plt.yscale('log') switch an axis to logarithmic — essential for data spanning orders of magnitude. 'symlog' is a log scale with a linear region near zero so it can display negative values. Use plt.grid(True, which='both') to show both major and minor (log-spaced) gridlines.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.logspace(0, 4, 50)   # 10^0 .. 10^4
plt.plot(x, x**2)

# log both axes
plt.xscale("log")
plt.yscale("log")

# symlog: log scale that handles negative/zero values
# plt.yscale("symlog", linthresh=1)

plt.title("Log-Log Plot")
plt.grid(True, which="both", ls="--", alpha=0.5)
plt.show()

Aspect Ratio & Spines

ax.set_aspect('equal') ensures equal scaling so circles stay circular — critical for geometry and maps. Spines are the axis border lines; hiding the top/right ('despining') gives a cleaner modern look popularized by Seaborn. You can also move spines to arbitrary data positions to build cross-hair axes.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()
theta = np.linspace(0, 2*np.pi, 100)
ax.plot(np.cos(theta), np.sin(theta))

# equal data units on x and y (a true circle)
ax.set_aspect("equal")

# hide the top and right spines
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# move the remaining spines to the center for a cross-hair
# ax.spines["left"].set_position(("data", 0))
# ax.spines["bottom"].set_position(("data", 0))

plt.show()
11

Text & Mathematical Expressions

Text in Axes

ax.text(x, y, s) places text at data coordinates by default. Pass transform=ax.transAxes to use axes-fraction coordinates (0,0 = bottom-left, 1,1 = top-right), which is how you pin labels to a corner regardless of the data range. ha/va control horizontal/vertical alignment.

matplotlib
import matplotlib.pyplot as plt

plt.plot([0, 1], [0, 1])
# ax.text(x, y, s)  -- x, y in data coords
plt.text(0.5, 0.5, "center of plot", ha="center", va="center",
         fontsize=14, color="red", rotation=30)

# align to axes fraction instead of data coords
plt.text(0.02, 0.98, "top-left", transform=plt.gca().transAxes,
         ha="left", va="top", bbox=dict(boxstyle="round", fc="yellow"))
plt.show()

suptitle & figtext

fig.suptitle() adds a title spanning the whole figure (above all subplots); reserve space for it with tight_layout(rect=[...]) or use layout='constrained'. fig.text(x, y, s) places text in figure-fraction coordinates (0,0 = bottom-left of the figure), useful for footers and source notes.

matplotlib
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2)
axes[0].plot([1, 2, 3])
axes[1].plot([3, 2, 1])

# figure-level title (above all axes)
fig.suptitle("Main Title", fontsize=16, fontweight="bold")

# arbitrary figure-coordinate text
fig.text(0.5, 0.01, "figure footer text", ha="center", fontsize=9)
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()

LaTeX Math (mathtext)

Matplotlib has a built-in mathtext engine: wrap LaTeX-like math in dollar signs ($...$) and use raw strings (r"..."). Common commands work: \frac, \sum, \int, Greek letters \alpha..\omega, superscripts x^2, subscripts x_i. No external LaTeX install is needed. Use r-strings so backslashes survive Python.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

# mathtext: enclose LaTeX in dollar signs
plt.title(r"$\sin(x)$ vs $x$")
plt.xlabel(r"$x$ (radians)")
plt.ylabel(r"$\sin(x)$")

# fractions, sums, greek letters
plt.text(2, 0.5, r"$\frac{a}{b} + \sum_{i=1}^{n} x_i^2$")
plt.text(2, -0.5, r"$\alpha, \beta, \gamma, \pi, \infty$")
plt.show()

Text Properties

Every text object accepts a rich set of properties: fontsize, fontweight ('bold'), fontstyle ('italic'), family ('serif'/'sans-serif'/'monospace'), color and a bbox dict for a background box. Text objects are returned so you can mutate them later (useful in animations and interactive updates).

matplotlib
import matplotlib.pyplot as plt

t = plt.text(0.5, 0.5, "Styled Text",
             fontsize=18, fontweight="bold",
             fontstyle="italic", family="serif",
             color="darkblue",
             bbox=dict(boxstyle="round,pad=0.5",
                       facecolor="lightyellow", edgecolor="black"))

# animate or update properties later
t.set_fontsize(24)
t.set_color("crimson")

plt.show()

Annotations (textcoords)

annotate's textcoords controls how xytext is interpreted: 'data' (default, data units), 'offset points' (points relative to xy — great for nudging labels off a point), 'axes fraction' (0-1 across the axes), or 'figure fraction'. Mixing coordinate systems lets callouts stay readable as you zoom.

matplotlib
import matplotlib.pyplot as plt

plt.plot([1, 2, 3, 4], [1, 4, 2, 3])

# xy in data coords, xytext in points OFFSET from xy
plt.annotate("offset text", xy=(2, 4),
             xytext=(15, 15), textcoords="offset points",
             arrowprops=dict(arrowstyle="->"))

# xytext in axes fraction
plt.annotate("axes corner", xy=(2, 4),
             xytext=(0.8, 0.8), textcoords="axes fraction",
             arrowprops=dict(arrowstyle="-|>"))

plt.show()
12

Saving Figures

savefig Basics

plt.savefig(path) writes the current figure to a file. Crucially, call it BEFORE plt.show() — show() finishes and may close/empty the figure, so saving afterwards produces a blank file. The file extension determines the format automatically (.png, .pdf, .svg, .jpg).

matplotlib
import matplotlib.pyplot as plt

plt.plot([1, 2, 3], [4, 5, 2])

# save BEFORE plt.show() -- show() clears the figure
plt.savefig("plot.png")
plt.savefig("plot.pdf")
plt.show()

Formats

Choose format by purpose: PNG for screen/web, PDF or SVG (vector) for print and editing because they scale without pixelation, JPG only for photo-like images (it loses sharpness on lines and text). Vector formats also produce much smaller files for plots with few elements. Pass format= to override the extension.

matplotlib
import matplotlib.pyplot as plt

plt.plot([1, 2, 3], [1, 4, 9])

# raster formats (pixels)
plt.savefig("plot.png")          # lossless, default
plt.savefig("plot.jpg", quality=95)
plt.savefig("plot.tiff", dpi=200)

# vector formats (scale without pixelation)
plt.savefig("plot.pdf")          # best for publications
plt.savefig("plot.svg")          # editable in Inkscape/Illustrator

# explicit format override
plt.savefig("plot.dat", format="png")

DPI & Resolution

DPI (dots per inch) sets the resolution of raster output: pixel dimensions = figsize * dpi. 72-100 DPI is fine for screens; 300+ DPI is required for print. Save with a vector format (PDF/SVG) to avoid resolution issues entirely. Set savefig.dpi in rcParams for consistent high-res output across a script.

matplotlib
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot([1, 2, 3], [1, 4, 9])

# dpi controls pixel density: width_px = figsize[0] * dpi
plt.savefig("low.png", dpi=72)     #  432 x 288 px
plt.savefig("high.png", dpi=300)   # 1800 x 1200 px

# set a global default DPI
plt.rcParams["savefig.dpi"] = 300
plt.savefig("global.png")

bbox_inches & Pad

By default savefig crops to the figure rectangle, which can clip labels and titles near the edge. bbox_inches='tight' expands the saved region to include all artists, then pad_inches adds a small margin around them. This is almost always what you want — set it as a default via rcParams['savefig.bbox']='tight'.

matplotlib
import matplotlib.pyplot as plt

plt.plot([1, 2, 3], [1, 4, 9])
plt.title("With Long Title That Might Be Cut Off")

# tight bbox trims whitespace and keeps labels inside
plt.savefig("tight.png", bbox_inches="tight", pad_inches=0.1)

# trim even more aggressively
plt.savefig("tighter.png", bbox_inches="tight", pad_inches=0)

# preview the tight bounding box
# bbox = plt.savefig("x.png", bbox_inches="tight")  # returns a Bbox

Transparent & Facecolor

transparent=True makes the figure background see-through (a PNG alpha channel) — ideal for overlaying plots on slides or web pages. facecolor sets the saved background color (it overrides the on-screen color). The axes background is controlled separately by ax.set_facecolor(). Combine both for layered designs.

matplotlib
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_facecolor("#f0f0f0")

# transparent background (alpha channel)
plt.savefig("transparent.png", transparent=True)

# explicit figure face color
plt.savefig("colored.png", facecolor="lightblue", edgecolor="none")

# control which elements are saved
plt.savefig("full.png", facecolor=fig.get_facecolor())
13

3D Plotting (mplot3d)

3D Axes Setup

Create a 3D axes with projection='3d' (either via add_subplot or subplot_kw in subplots). All 2-D methods work plus z-specific ones (set_zlabel, plot_surface, scatter, etc.). 3D support is via the mplot3d toolkit, which is bundled with Matplotlib and needs no extra install.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
# add a 3D axes via projection
ax = fig.add_subplot(projection="3d")

# or from subplots
# fig, ax = plt.subplots(subplot_kw={"projection": "3d"})

ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Z")
ax.set_title("3D Axes")

plt.show()

3D Scatter

ax.scatter(x, y, z) on a 3D axes plots points in space; the s and c arguments work just like 2-D scatter. Add fig.colorbar(sc) to show the color mapping. 3D scatter is great for exploring clusters, but be aware that static 3D plots can hide depth — rotate interactively or use color to encode one dimension.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
n = 200
x = rng.normal(0, 1, n)
y = rng.normal(0, 1, n)
z = rng.normal(0, 1, n)
c = np.sqrt(x**2 + y**2 + z**2)

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
sc = ax.scatter(x, y, z, c=c, cmap="viridis")
fig.colorbar(sc, label="distance from origin")
ax.set_title("3D Scatter")
plt.show()

3D Surface

plot_surface(X, Y, Z) draws a colored surface; X, Y, Z must be 2-D arrays of matching shape produced by np.meshgrid. rstride/cstride control how many grid points are skipped (lower = smoother but slower). cmap colors by Z; add a colorbar (with shrink to fit a 3D figure).

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-5, 5, 80)
y = np.linspace(-5, 5, 80)
X, Y = np.meshgrid(x, y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
surf = ax.plot_surface(X, Y, Z, cmap="coolwarm",
                       rstride=1, cstride=1,
                       linewidth=0, antialiased=True)
fig.colorbar(surf, shrink=0.5, aspect=10)
ax.set_title("3D Surface")
plt.show()

3D Wireframe & Contour

plot_wireframe draws only the grid lines (lighter than a full surface, good for showing structure). ax.contourf with zdir='z' and offset projects filled contours onto a plane beneath the surface — a common '2D-under-3D' visualization. Combine surface + projected contours for rich terrain plots.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 30)
y = np.linspace(-3, 3, 30)
X, Y = np.meshgrid(x, y)
Z = X * np.exp(-X**2 - Y**2)

fig = plt.figure()
ax = fig.add_subplot(projection="3d")
ax.plot_wireframe(X, Y, Z, color="black", rstride=1, cstride=1)
ax.set_title("Wireframe")

# 3D contour (filled) projected at the bottom
fig = plt.figure()
ax = fig.add_subplot(projection="3d")
ax.contourf(X, Y, Z, zdir="z", offset=-0.5, cmap="viridis")
ax.set_title("3D Contour")
plt.show()

3D Bar

ax.bar3d(x, y, z, dx, dy, dz) draws rectangular bars at positions (x, y) starting at base z with widths dx, dy and height dz. shade=True adds lighting for a 3D feel. Flatten coordinate arrays with .ravel() when starting from a meshgrid. 3D bars can be hard to read — prefer grouped 2D bars when possible.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.add_subplot(projection="3d")

x = np.arange(3)
y = np.arange(3)
X, Y = np.meshgrid(x, y)
X, Y = X.ravel(), Y.ravel()
Z = np.zeros_like(X)
dz = np.array([1, 2, 3, 2, 3, 1, 3, 1, 2])

ax.bar3d(X, Y, Z, dx=0.6, dy=0.6, dz=dz,
         shade=True, color="tab:orange")
ax.set_title("3D Bar Chart")
plt.show()
14

Contour Plots

Contour Lines

plt.contour(X, Y, Z) draws isolines where Z is constant. X, Y come from meshgrid. levels sets the number (or exact values) of contours. plt.clabel(cs) labels each line with its value. Line contours are precise but sparse; combine with contourf for filled regions.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)

# line contours
cs = plt.contour(X, Y, Z, levels=15, colors="black", linewidths=0.8)
plt.clabel(cs, inline=True, fontsize=8)
plt.title("Contour Lines")
plt.colorbar(cs)
plt.show()

Filled Contours

plt.contourf (note the 'f') fills the regions between contour levels with color, giving a smooth heat-map look. Like contour it needs gridded X, Y, Z. levels controls resolution. contourf is the standard way to visualize 2-D scalar fields such as temperature, elevation or probability density.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-2, 2, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z = X**2 + Y**2

cf = plt.contourf(X, Y, Z, levels=20, cmap="viridis")
plt.colorbar(cf, label="Z")
plt.title("Filled Contour")
plt.xlabel("X")
plt.ylabel("Y")
plt.show()

Labeling Contours (clabel)

plt.clabel(cs) annotates contour lines with their Z value; inline=True breaks the line to fit the label. fmt formats the number ('%.2f' or a function). Pass levels=[...] to label only specific isolines. Labeled contours convey exact values without needing a colorbar.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = X * np.exp(-X**2 - Y**2)

cs = plt.contour(X, Y, Z, 10, colors="black")
plt.clabel(cs, inline=True, fontsize=9, fmt="%.2f")
# label only specific levels
# plt.clabel(cs, levels=[-0.1, 0, 0.1])
plt.title("Labeled Contours")
plt.show()

Colorbar

plt.colorbar(mappable) adds a color key — always pass the contour/imshow/scatter return value so it knows the mapping. set_label/set_ticks customize it. In subplots, colorbars can steal space from an axis; use the ax= argument or fig.colorbar(mappable, ax=axes) to control placement across multiple axes.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 5, 100)
y = np.linspace(0, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) + np.cos(Y)

cf = plt.contourf(X, Y, Z, cmap="RdBu")

# basic colorbar
cb = plt.colorbar(cf)
cb.set_label("Z value")
cb.set_ticks([-2, -1, 0, 1, 2])

# place colorbar with specific size and padding
# cb = plt.colorbar(cf, fraction=0.046, pad=0.04)

plt.title("Colorbar Example")
plt.show()

meshgrid & griddata

Contour/contourf need data on a regular grid. For scattered samples, use scipy.interpolate.griddata to interpolate onto a meshgrid (methods: 'linear', 'nearest', 'cubic'). np.mgrid[...] is a convenient shorthand for meshgrid over a range. Plotting the original points on top shows where data is sparse.

matplotlib
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import griddata

# scattered (x, y, z) samples
rng = np.random.default_rng(0)
pts = rng.uniform(-2, 2, (200, 2))
z = np.sin(pts[:,0]) * np.cos(pts[:,1])

# build a regular grid
grid_x, grid_y = np.mgrid[-2:2:100j, -2:2:100j]

# interpolate scattered data onto the grid
grid_z = griddata(pts, z, (grid_x, grid_y), method="cubic")

plt.contourf(grid_x, grid_y, grid_z, levels=20, cmap="plasma")
plt.scatter(pts[:,0], pts[:,1], c="white", s=5, alpha=0.5)
plt.colorbar(label="z")
plt.title("Scattered -> Grid -> Contour")
plt.show()
15

Image Display (imshow)

imshow Basics

plt.imshow(arr) displays a 2-D array as a heatmap or a 3-D (H, W, 3) array as an RGB image. By default the origin is top-left (origin='upper'), matching image conventions — set origin='lower' for math-style axes. plt.axis('off') hides ticks for clean image display.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

# a 2D array is shown as a heatmap (color = value)
rng = np.random.default_rng(0)
img = rng.random((10, 10))

plt.imshow(img, cmap="viridis")
plt.colorbar(label="value")
plt.title("imshow of a 2D array")
plt.show()

# a 3D array (H, W, 3 or 4) is shown as an RGB(A) image
rgb = rng.integers(0, 256, (50, 50, 3), dtype=np.uint8)
plt.imshow(rgb)
plt.axis("off")
plt.title("RGB image")
plt.show()

Colormap & Colorbar

extent=[xmin, xmax, ymin, max] maps array pixels to data coordinates; without it imshow uses pixel indices. origin='lower' puts row 0 at the bottom (math convention). aspect='auto' stretches the image to fill the axes; the default 'equal' preserves square pixels. Pass the image object to colorbar.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 200)
y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = np.sin(X**2 + Y**2) / (X**2 + Y**2 + 1)

im = plt.imshow(Z, extent=[-3, 3, -3, 3], origin="lower",
                cmap="RdBu", aspect="auto")
cb = plt.colorbar(im)
cb.set_label("intensity")
plt.title("imshow with extent")
plt.show()

Interpolation

interpolation controls how pixels are blended when upscaled: 'none'/'nearest' shows crisp blocks (good for showing raw pixel data), 'bilinear'/'bicubic' smooth between pixels (good for heatmaps). Interpolation only changes appearance, not the underlying data. For small arrays you often WANT 'nearest' so individual cells are visible.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(1)
small = rng.random((5, 5))

methods = ["none", "bilinear", "bicubic", "nearest"]
fig, axes = plt.subplots(1, 4, figsize=(12, 3))
for ax, m in zip(axes, methods):
    ax.imshow(small, interpolation=m, cmap="magma")
    ax.set_title(m)
    ax.axis("off")
plt.tight_layout()
plt.show()

vmin / vmax & norm

vmin/vmax fix the data range mapped to the colormap (clipping outliers). For non-linear mappings use norm=: LogNorm for data spanning orders of magnitude, TwoSlopeNorm to center a diverging map on a meaningful midpoint (e.g. 0 for anomalies). norm and vmin/vmax are mutually exclusive — pass only one.

matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import LogNorm, TwoSlopeNorm

rng = np.random.default_rng(2)
Z = rng.random((50, 50)) * 1000 + 1

# clamp the colormap range
plt.imshow(Z, vmin=100, vmax=500, cmap="viridis")
plt.colorbar()
plt.title("clipped range")
plt.show()

# log-scaled colors
plt.imshow(Z, norm=LogNorm(vmin=1, vmax=1001), cmap="plasma")
plt.colorbar()
plt.title("log scale")
plt.show()

# diverging norm centered on a midpoint
# norm = TwoSlopeNorm(vmin=-1, vcenter=0, vmax=1)

Display Image from File

mpimg.imread loads an image into a NumPy array (float 0-1 for PNG, 0-255 uint8 for JPEG). plt.imshow displays it; plt.imsave writes an array back to disk. RGB images keep their colors; pass cmap='gray' for single-channel images. Note PIL/pillow is more capable for non-trivial image processing.

matplotlib
import matplotlib.pyplot as plt
import matplotlib.image as mpimg

# read an image file into a numpy array
img = mpimg.imread("photo.png")     # shape (H, W, 3) or (H, W, 4)
print(img.shape, img.dtype)

plt.imshow(img)
plt.axis("off")
plt.show()

# save the (possibly modified) array back to a file
# plt.imsave("out.png", img)

# drop the alpha channel if present
if img.shape[-1] == 4:
    img = img[..., :3]
16

Polar Plots

Polar Axes

projection='polar' turns an axes into polar coordinates: x is the angle (theta, radians) and y is the radius (r). All standard methods work but operate in (theta, r). Polar plots are ideal for directional data, periodic functions, and circular symmetries. Set ticks with ax.set_thetagrids and ax.set_rgrids.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

# create a polar axes via projection
fig = plt.figure()
ax = fig.add_subplot(projection="polar")

# or from subplots
# fig, ax = plt.subplots(subplot_kw={"projection": "polar"})

theta = np.linspace(0, 2*np.pi, 100)
ax.plot(theta, np.sin(2*theta))
plt.show()

Polar Line Plot

On a polar ax, plot(theta, r) draws curves where theta is the angle and r the radius. set_rmax clips the radial range; set_rticks and set_thetagrids customize the radial/angular tick positions. Polar line plots cleanly show periodic functions — a sine wave wraps into a closed loop.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

theta = np.linspace(0, 2*np.pi, 200)

fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
ax.plot(theta, 1 + np.cos(theta), label="cardioid")
ax.plot(theta, np.abs(np.sin(2*theta)), label="rose")
ax.set_rmax(2.0)
ax.set_rticks([0.5, 1.0, 1.5])
ax.set_thetagrids([0, 90, 180, 270])
ax.legend(loc="upper right")
plt.show()

Radar / Spider Chart

A radar chart is a polar line plot with one axis per category. The trick is to append the first value/angle to the end so the curve closes into a loop, and fill the interior with alpha. Set xticks to the category angles and label them. Radars compare multivariate profiles but can mislead — use sparingly.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

labels = ["Speed", "Power", "Range", "Comfort", "Price"]
values = [8, 7, 6, 5, 9]
N = len(labels)

# close the loop
angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist()
values += values[:1]
angles += angles[:1]

fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
ax.plot(angles, values, "o-")
ax.fill(angles, values, alpha=0.25)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(labels)
ax.set_title("Radar Chart")
plt.show()

Polar Bar (Rose)

A rose (or polar bar) chart uses ax.bar on a polar axes — each bar occupies an angular sector. Useful for directional histograms like wind direction or wave period distributions. Color the bars by radius (as shown) for an extra dimension. bottom= can stack or offset bars from the center.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
N = 12
theta = np.linspace(0, 2*np.pi, N, endpoint=False)
widths = 2*np.pi / N
radii = rng.uniform(1, 5, N)

fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
bars = ax.bar(theta, radii, width=widths, bottom=0,
              edgecolor="black")

# color bars by radius
for r, bar in zip(radii, bars):
    bar.set_facecolor(plt.cm.viridis(r / radii.max()))
ax.set_title("Rose Diagram")
plt.show()

Polar Settings

set_theta_zero_location('N') moves 0 radians to the top and set_theta_direction(-1) makes angles increase clockwise — the compass convention. set_rlabel_position(angle) relocates the radial tick labels to any angle to avoid overlapping data. These tweaks convert a math polar plot into a directional/compass plot.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
ax.plot(np.linspace(0, 2*np.pi, 100), np.linspace(0, 1, 100))

# rotate 0 degrees to the top, go clockwise
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)

# radial limits and ticks
ax.set_rlim(0, 1.2)
ax.set_rticks([0.25, 0.5, 0.75, 1.0])
ax.set_rlabel_position(135)   # move radial labels out of the way

# label directions in degrees
ax.set_thetagrids([0, 90, 180, 270], labels=["E","N","W","S"])
plt.show()
17

Twin Axes

twinx (Secondary Y)

ax.twinx() creates a new axes that shares the x-axis but has its own y-axis on the right — the standard way to plot two quantities with different units (e.g. temperature and humidity). Color each axis and its ticks to match the curve so readers know which scale belongs to which series.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

fig, ax1 = plt.subplots()
ax1.plot(x, np.sin(x), "b-")
ax1.set_xlabel("x")
ax1.set_ylabel("sin(x)", color="b")
ax1.tick_params(axis="y", labelcolor="b")

# a second axes sharing the same x, with an independent y
ax2 = ax1.twinx()
ax2.plot(x, x*10, "r-")
ax2.set_ylabel("10*x", color="r")
ax2.tick_params(axis="y", labelcolor="r")

plt.title("twinx: two y-scales")
plt.show()

twiny (Secondary X)

ax.twiny() is the horizontal counterpart: a new axes sharing the y-axis but with its own x-axis on top. Use it to show the same data against two x-scales (e.g. meters and feet, or time in seconds and minutes). The same color-coding tip applies — match axis color to its curve.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

y = np.linspace(0, 10, 50)

fig, ax1 = plt.subplots()
ax1.plot(np.exp(y), y, "g-")
ax1.set_ylabel("y")
ax1.set_xlabel("exp(y)", color="g")

# second axes sharing y, independent x on top
ax2 = ax1.twiny()
ax2.plot(y**2, y, "m-")
ax2.set_xlabel("y^2", color="m")

plt.title("twiny: two x-scales")
plt.show()

Combined Twin Chart

When twinning axes, each axes has its own legend. To show one combined legend, collect the line handles from both axes into a list and call ax1.legend(lines, labels). This is the idiomatic way to keep a single key for a dual-axis chart so readers can identify both series at once.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

t = np.linspace(0, 5, 100)
temp_c = 20 + 5*np.sin(t)         # Celsius
humidity = 60 - 5*t               # percent

fig, ax1 = plt.subplots(figsize=(8, 4))

l1, = ax1.plot(t, temp_c, "r-o", label="Temperature (C)")
ax1.set_xlabel("time (s)")
ax1.set_ylabel("Temperature (C)", color="r")
ax1.tick_params(axis="y", labelcolor="r")

ax2 = ax1.twinx()
l2, = ax2.plot(t, humidity, "b-s", label="Humidity (%)")
ax2.set_ylabel("Humidity (%)", color="b")
ax2.tick_params(axis="y", labelcolor="b")

# combine legends from both axes
lines = [l1, l2]
ax1.legend(lines, [l.get_label() for l in lines], loc="upper right")
plt.title("Temperature & Humidity")
plt.show()

Despine Twin Axes

Twin axes naturally produce a doubled top border. Hide the top spines on both axes for a cleaner look, and color the left/right spines to match their respective y-axis. Combined with colored tick labels this clearly signals which scale belongs to which curve without cluttering the chart.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

fig, ax1 = plt.subplots()
ax2 = ax1.twinx()

ax1.plot(x, np.sin(x), color="tab:blue")
ax2.plot(x, np.cos(x), color="tab:orange")

# remove the top spine so the two axes don't double-border
ax1.spines["top"].set_visible(False)
ax2.spines["top"].set_visible(False)

# color the spines to match their axes
ax1.spines["left"].set_color("tab:blue")
ax2.spines["right"].set_color("tab:orange")

plt.title("Clean Twin Axes")
plt.show()

Twin with Different Units

Sometimes you don't need a second plotted series — you just want a second axis with converted units (km/miles, C/F). Create twiny(), align the limits with set_xlim, then manually set xticks/xticklabels to the converted values. This annotates one data series with two unit systems.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

km = np.linspace(0, 100, 50)
mile = km * 0.621371

fig, ax1 = plt.subplots()
ax1.plot(km, np.sin(km/10), "tab:purple")
ax1.set_xlabel("distance")
ax1.set_ylabel("signal", color="tab:purple")

ax2 = ax1.twiny()
ax2.set_xlim(ax1.get_xlim())          # align x limits
# relabel the top ticks in miles
new_ticks = np.array([0, 25, 50, 75, 100])
ax2.set_xticks(new_ticks)
ax2.set_xticklabels([f"{m:.0f}" for m in new_ticks*0.621371])
ax2.set_xlabel("miles")
plt.show()
18

Error Bars & Filled Areas

errorbar

plt.errorbar(x, y, yerr) plots points with vertical error bars — the standard for experimental data with uncertainty. fmt is a plot format string ('o-' = markers + line). ecolor, elinewidth and capsize style the error bars. Use xerr= for horizontal errors, or both for 2-D uncertainties.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 5, 10)
y = np.exp(-x)
yerr = 0.1 + 0.1*x      # error grows with x

plt.errorbar(x, y, yerr=yerr, fmt="o-", color="tab:blue",
             ecolor="gray", elinewidth=1.5, capsize=4,
             markersize=6, label="exp(-x) +/- err")
plt.legend()
plt.title("Error Bar Plot")
plt.show()

Asymmetric Errors

For asymmetric uncertainties pass yerr as a 2 x N array: row 0 is the lower (downward) errors and row 1 the upper (upward) errors. This is common when errors are skewed (e.g. Poisson counts). The same pattern applies to xerr for asymmetric horizontal uncertainties.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(5)
y = np.array([2, 3, 5, 4, 6])
# separate lower and upper errors (each a 1-D array)
yerr = [[0.5, 1, 0.8, 0.6, 1.2],   # lower
        [1.0, 0.8, 1.5, 1.0, 0.7]]  # upper

plt.errorbar(x, y, yerr=yerr, fmt="s", color="tab:green",
             capsize=5)
plt.title("Asymmetric Error Bars")
plt.show()

# horizontal asymmetric errors work the same way
# xerr = [[...lower...], [...upper...]]

fill_between

fill_between(x, y1, y2) shades the region between two y-curves — perfect for confidence intervals, error bands and ranges. where= limits the fill to where a boolean array is True (use interpolate=True if curves cross). alpha < 1 keeps the underlying curve visible through the band.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

plt.plot(x, y, "b-", label="mean")
# shade +/- one standard deviation
plt.fill_between(x, y - std, y + std, color="blue", alpha=0.2,
                 label="+/- 1 std")

# shade only where a condition holds
plt.fill_between(x, y, 0, where=(y > 0), color="green", alpha=0.2,
                 label="y > 0")
plt.legend()
plt.show()

stackplot

plt.stackplot(x, y1, y2, ...) stacks multiple series on top of each other, showing both totals and composition over a continuous x (usually time). Unlike stacked bars it is for continuous data. The total height is the sum of all series. Use distinct colors with alpha so boundaries between layers are clear.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

years = np.arange(2010, 2021)
a = np.array([10, 12, 14, 15, 17, 18, 20, 22, 23, 25, 27])
b = np.array([ 5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15])
c = np.array([ 3,  4,  4,  5,  6,  6,  7,  8,  8,  9, 10])

plt.stackplot(years, a, b, c, labels=["A", "B", "C"],
              colors=["#4C72B0", "#55A868", "#C44E52"], alpha=0.8)
plt.legend(loc="upper left")
plt.title("Stacked Area Chart")
plt.xlabel("year")
plt.ylabel("value")
plt.show()

fill (Polygon)

plt.fill(x, y) closes and shades an arbitrary polygon — useful for highlighting regions or shading under a curve. To shade under a curve, concatenate the x values with their reverse and the y values with zeros. For multiple disjoint filled regions, pass 2-D arrays or call fill repeatedly.

matplotlib
import matplotlib.pyplot as plt
import numpy as np

# fill a closed polygon defined by its vertices
x = [0, 1, 1, 0, 0]
y = [0, 0, 1, 1, 0]
plt.fill(x, y, color="tab:cyan", alpha=0.5)

# shade under a curve
xs = np.linspace(0, 2*np.pi, 100)
ys = np.sin(xs)
plt.fill(np.r_[xs, xs[::-1]],
         np.r_[ys, np.zeros_like(ys)],
         color="tab:orange", alpha=0.3)

plt.title("Filled Polygons")
plt.show()
19

Seaborn Integration

Seaborn Intro

Seaborn is a high-level statistical plotting library built on top of Matplotlib: it accepts pandas DataFrames, handles grouping/aggregation, and uses nicer defaults. Every Seaborn function returns a Matplotlib Axes (or Figure), so you can keep customizing with plt/ax calls. Call sns.set_theme() once to switch to Seaborn's style.

matplotlib
# install seaborn
pip install seaborn

import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

# Seaborn is built ON matplotlib: any sns plot returns an Axes
# and accepts an ax= argument, so you can mix freely.

sns.set_theme()        # apply seaborn's default style
tips = sns.load_dataset("tips")

ax = sns.scatterplot(data=tips, x="total_bill", y="tip", hue="time")
ax.set_title("Tips dataset")
plt.show()

Seaborn Styles

sns.set_style sets the axes look (grid, background), set_context scales fonts/lines for the output medium ('paper', 'talk', 'poster'), and set_palette sets the color cycle. sns.despine() removes the top/right spines. These compose into a polished default look with one or two lines.

matplotlib
import seaborn as sns
import matplotlib.pyplot as plt

# five built-in themes: darkgrid, whitegrid, dark, white, ticks
sns.set_style("whitegrid")
sns.set_context("notebook")   # paper, notebook, talk, poster

# set the color palette
sns.set_palette("Set2")

import numpy as np
for i in range(4):
    plt.plot(np.arange(10), np.arange(10)+i)
plt.title("Seaborn-styled")
plt.show()

# remove the top/right spines (despine) for a clean look
# sns.despine()

relplot (relational)

sns.relplot is the figure-level interface for relational plots: kind='scatter' (default) or 'line'. col/row split the data into subplots (faceting) automatically — far easier than manual subplots. It returns a FacetGrid whose .fig and .axes let you drop down to Matplotlib for fine-tuning titles and layout.

matplotlib
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset("tips")

# scatter or line, faceted by a categorical variable
g = sns.relplot(data=tips, x="total_bill", y="tip",
                hue="smoker", style="time", col="day",
                kind="scatter", height=3, aspect=1.2)

g.fig.suptitle("Tips by day", y=1.03)
plt.show()

histplot & kdeplot

sns.histplot is the modern replacement for the deprecated distplot: it draws histograms and can overlay a kernel density estimate (kde=True). stat='density' normalizes, and common_norm=False keeps groups normalized independently. sns.kdeplot draws just the smooth density curve, useful for comparing distributions.

matplotlib
import seaborn as sns
import matplotlib.pyplot as plt

tips = sns.load_dataset("tips")

# histogram with an overlaid KDE (density curve)
sns.histplot(data=tips, x="total_bill", bins=20, kde=True,
             hue="time", stat="density", common_norm=False)
plt.title("Histogram + KDE")
plt.show()

# standalone KDE
plt.figure()
sns.kdeplot(data=tips, x="total_bill", hue="time", fill=True)
plt.show()

heatmap

sns.heatmap draws a color-coded matrix with optional cell annotations (annot=True). center=0 with a diverging cmap (coolwarm) makes positive/negative correlations visually distinct. It wraps plt.imshow + plt.text + colorbar, saving a lot of boilerplate for correlation matrices and confusion matrices.

matplotlib
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np

# correlation matrix
tips = sns.load_dataset("tips")
num = tips.select_dtypes("number").drop(columns=["size"])
corr = num.corr()

sns.heatmap(corr, annot=True, cmap="coolwarm", center=0,
            vmin=-1, vmax=1, square=True,
            linewidths=0.5, fmt=".2f")
plt.title("Correlation Heatmap")
plt.show()

pairplot & violinplot

sns.pairplot creates a grid of scatter plots for every pair of numeric columns, colored by a categorical hue — the fastest way to explore relationships in a new dataset. sns.violinplot combines a box plot with a density estimate, showing the distribution shape per group. Both are pure Matplotlib underneath and can be customized further.

matplotlib
import seaborn as sns
import matplotlib.pyplot as plt

iris = sns.load_dataset("iris")

# scatter matrix: every numeric column vs every other
g = sns.pairplot(iris, hue="species", height=2.5)
g.fig.suptitle("Iris pairplot", y=1.01)
plt.show()

# violin plot: box + kernel density on each side
plt.figure()
sns.violinplot(data=iris, x="species", y="sepal_length",
               inner="quartile")
plt.show()
20

Interactive & Animation

Slider Widget

matplotlib.widgets.Slider adds an interactive slider. Create an axes for it (often at the figure bottom via plt.axes([left, bottom, width, height] in figure fraction)), wire a callback to on_changed, and update your plot data inside it. Call fig.canvas.draw_idle() to refresh. Requires an interactive backend (not Agg).

matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import numpy as np

fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.25)

x = np.linspace(0, 10, 500)
line, = ax.plot(x, np.sin(x))

# axes for the slider
ax_freq = plt.axes([0.2, 0.1, 0.6, 0.03])
slider = Slider(ax_freq, "freq", 0.1, 5.0, valinit=1.0)

def update(val):
    line.set_ydata(np.sin(slider.val * x))
    fig.canvas.draw_idle()

slider.on_changed(update)
plt.show()

Buttons

matplotlib.widgets.Button adds a clickable button. Like the slider it needs its own axes (positioned in figure-fraction coordinates). The callback receives an event object you usually ignore. Combine multiple widgets (sliders, buttons, checkboxes) to build simple data-exploration dashboards directly in Matplotlib.

matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import Button
import numpy as np

fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.2)
(ln,) = ax.plot(np.random.rand(10))

ax_btn = plt.axes([0.7, 0.05, 0.2, 0.075])
btn = Button(ax_btn, "Resample")

def reshuffle(event):
    ln.set_ydata(np.random.rand(10))
    fig.canvas.draw_idle()

btn.on_clicked(reshuffle)
plt.show()

CheckButtons

CheckButtons renders a list of checkboxes. The callback receives the text of the toggled label, so you match on it to flip each artist's visibility. Use it to let viewers toggle series on and off. As with all widgets, you need an interactive backend and the window must stay open.

matplotlib
import matplotlib.pyplot as plt
from matplotlib.widgets import CheckButtons
import numpy as np

x = np.linspace(0, 5, 200)
l1, = plt.plot(x, np.sin(x), label="sin")
l2, = plt.plot(x, np.cos(x), label="cos")
plt.subplots_adjust(right=0.8)

rax = plt.axes([0.82, 0.4, 0.15, 0.2])
check = CheckButtons(rax, ["sin", "cos"], [True, True])

def toggle(label):
    if label == "sin": l1.set_visible(not l1.get_visible())
    if label == "cos": l2.set_visible(not l2.get_visible())
    plt.draw()

check.on_clicked(toggle)
plt.show()

Cursor & Mouse Events

mpl_connect links events (button_press_event, motion_notify_event, key_press_event, scroll_event) to callbacks. The event object carries event.xdata/event.ydata (data coords) and event.inaxes (which axes, or None). This is the basis for custom interactions — crosshairs, click-to-annotate, custom zoom tools.

matplotlib
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot([1, 2, 3, 4], [1, 4, 2, 3])

def on_click(event):
    if event.inaxes != ax:
        return
    print(f"clicked at x={event.xdata:.2f} y={event.ydata:.2f}")

def on_move(event):
    if event.inaxes == ax:
        ax.set_title(f"x={event.xdata:.2f}, y={event.ydata:.2f}")
        fig.canvas.draw_idle()

fig.canvas.mpl_connect("button_press_event", on_click)
fig.canvas.mpl_connect("motion_notify_event", on_move)
plt.show()

Animation (FuncAnimation)

FuncAnimation drives an animation by calling update(frame) repeatedly. update must return the changed artists (as a tuple) so blit=True can redraw only them for speed. interval is the delay between frames in ms. Saving requires a writer ('ffmpeg' for mp4, 'pillow' for gif). Keep the animation object (ani) referenced or it gets garbage-collected and stops.

matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, ax = plt.subplots()
x = np.linspace(0, 2*np.pi, 200)
line, = ax.plot(x, np.sin(x))

def update(frame):
    line.set_ydata(np.sin(x + frame / 10.0))
    return line,

ani = animation.FuncAnimation(fig, update, frames=100,
                              interval=50, blit=True, repeat=True)
# save to a file (requires ffmpeg or pillow installed)
# ani.save("wave.mp4", writer="ffmpeg")
# ani.save("wave.gif", writer="pillow")
plt.show()

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.