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.
# 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.
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.
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.
# 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 inlineBackends & 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.
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 backendLine 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.
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.
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'.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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+.
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.
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.
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.
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.
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()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.
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).
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.
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.
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.
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()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.
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.
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.
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).
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.
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()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).
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.
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.
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'.
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 BboxTransparent & 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.
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())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.
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.
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).
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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]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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
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.
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.
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.
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.
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()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.
# 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.
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.
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.
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.
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.
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()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).
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.
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.
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.
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.
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()Verwandte Matplotlib-Snippets
Copy-paste ready code for common tasks.
Line Plot
Plot lines with markers, colors, and styles.
Bar Chart
Draw vertical, horizontal, and grouped bars.
Scatter Plot
Visualize relationships with size and color encodings.
Subplots
Arrange multiple axes with subplots and GridSpec.
Labels and Legend
Annotate plots with text, arrows, and legend options.
Styles and Themes
Apply built-in styles and customize rcParams.
Save Figure
Export figures to PNG, PDF, and SVG with DPI control.
3D Plot
Render surfaces and scatter in 3D.
Was this helpful?