How to Use matplotlib

Updated May 2026
matplotlib is Python's foundational plotting library, capable of producing every type of scientific figure from simple line plots to complex multi-panel layouts with customized colormaps, annotations, and insets. It gives you complete control over every visual element in a figure, which is why it remains the standard for generating publication-quality plots that meet the exacting formatting requirements of scientific journals, even as higher-level libraries like Seaborn build convenience layers on top of it.

Import matplotlib's pyplot module with the standard alias: import matplotlib.pyplot as plt. The pyplot interface provides two ways to create figures. The quick, stateful approach uses plt functions directly: plt.plot(x, y), plt.xlabel('Time'), plt.show(). The object-oriented approach creates figure and axes objects explicitly: fig, ax = plt.subplots(), then ax.plot(x, y), ax.set_xlabel('Time'). The object-oriented approach is strongly recommended for anything beyond throwaway exploration because it makes the code explicit about which axes receives which data, essential for multi-panel figures and non-trivial customization.

Step 1: Create Basic Plots

Line plots display continuous data, time series, and function curves. ax.plot(x, y) draws connected line segments. ax.plot(x, y, 'r--o') uses a format string: 'r' for red, '--' for dashed line, 'o' for circle markers. Keyword arguments give finer control: ax.plot(x, y, color='#2563EB', linewidth=1.5, linestyle='-', marker='o', markersize=4, label='Experiment A'). Plotting multiple lines on the same axes is as simple as calling ax.plot() multiple times before calling plt.show(). Each call adds another line to the same axes, and ax.legend() creates a legend using the label parameters.

Scatter plots show relationships between two variables. ax.scatter(x, y) creates a basic scatter plot. The c parameter maps a third variable to color: ax.scatter(x, y, c=z, cmap='viridis') colors each point by z using the viridis colormap, revealing three-dimensional relationships in a 2D plot. The s parameter maps to marker size. Adding a colorbar with plt.colorbar() creates a labeled color scale. For large datasets (10000+ points), ax.scatter with alpha=0.3 (transparency) or ax.hexbin(x, y, gridsize=30, cmap='Blues') (hexagonal binning) prevents overplotting.

Histograms visualize distributions. ax.hist(data, bins=30) creates a histogram with 30 bins. ax.hist(data, bins='auto') lets NumPy choose the optimal bin count using the Freedman-Diaconis rule. ax.hist([data1, data2], bins=30, label=['Group A', 'Group B'], alpha=0.7) overlays two histograms for comparison. density=True normalizes the histogram so the area sums to 1, making it comparable to probability density functions. For kernel density estimates, Seaborn's sns.kdeplot(data) provides smooth density curves.

Bar charts compare categories. ax.bar(categories, values) creates vertical bars. ax.barh(categories, values) creates horizontal bars, preferred when category labels are long. Grouped bar charts use offset x-positions: compute x positions for each group, set bar width to less than the group spacing, and call ax.bar() for each group with offset positions. Stacked bar charts use the bottom parameter: ax.bar(x, values1), ax.bar(x, values2, bottom=values1) stacks values2 on top of values1. ax.errorbar(x, y, yerr=errors, fmt='o', capsize=3) creates a plot with error bars, the standard for showing experimental uncertainty.

Step 2: Customize Appearance

Axis labels and titles communicate what the plot shows. ax.set_xlabel('Temperature (C)', fontsize=10) sets the x-axis label. ax.set_ylabel('Reaction Rate (mol/s)', fontsize=10) sets the y-axis. ax.set_title('Effect of Temperature on Reaction Rate', fontsize=12) adds a title. Always include units in axis labels: "Temperature" is incomplete, "Temperature (C)" tells the reader everything they need. For multi-panel figures, omit individual titles and use fig.suptitle() for a single figure title, then label panels with ax.text(0.02, 0.95, '(a)', transform=ax.transAxes, fontweight='bold').

Axis limits and ticks control the coordinate system. ax.set_xlim(0, 100) and ax.set_ylim(0, 50) set the range. ax.set_xticks([0, 25, 50, 75, 100]) places tick marks at specific positions. ax.set_xticklabels(['Start', '25%', '50%', '75%', 'End']) replaces numeric tick labels with text. ax.tick_params(labelsize=8) sets the tick label font size. ax.set_xscale('log') switches to logarithmic scale. ax.invert_yaxis() inverts the y-axis direction (common for depth profiles, ocean data, and certain astronomical conventions).

Colors and styles define the visual identity of your figures. matplotlib accepts named colors ('blue', 'red'), hex codes ('#2563EB'), RGB tuples ((0.15, 0.39, 0.92)), and colormap references. For scientific publications, avoid the default color cycle and use colorblind-friendly palettes. The 'tab10' colormap works for up to 10 categories. For continuous data, 'viridis' (perceptually uniform, colorblind safe) is the default recommendation. 'coolwarm' works for diverging data (centered on zero). 'Greys' works when figures must be legible in grayscale printing.

Grid lines and spines refine the visual structure. ax.grid(True, alpha=0.3, linestyle='--') adds subtle grid lines. ax.spines['top'].set_visible(False) and ax.spines['right'].set_visible(False) remove the top and right borders, creating the cleaner "L-shaped" frame preferred by many journals. ax.axhline(y=0, color='black', linewidth=0.5) adds a horizontal reference line. ax.axvspan(xmin=2, xmax=5, alpha=0.1, color='gray') shades a vertical region, useful for highlighting significant intervals, treatment periods, or confidence regions.

Step 3: Build Multi-Panel Figures

fig, axes = plt.subplots(2, 3, figsize=(10, 6)) creates a 2-row, 3-column grid of panels. The axes variable is a 2D NumPy array of Axes objects: axes[0, 0] is the top-left panel, axes[1, 2] is the bottom-right. Each panel is customized independently: axes[0, 0].plot(x, y), axes[0, 0].set_xlabel('Time'). The figsize parameter (width, height in inches) controls the overall figure dimensions. For publications, match the journal's column width: 3.5 inches for single-column, 7.25 inches for double-column.

Shared axes prevent redundant labels and ensure consistent scales. fig, axes = plt.subplots(2, 2, sharex=True, sharey=True) creates panels where all x-axes have the same range and all y-axes have the same range. Only the left-column panels show y-axis labels, and only the bottom-row panels show x-axis labels, automatically eliminating redundancy. This is the correct layout for comparing related datasets or conditions.

GridSpec provides flexible layouts where panels have different sizes. fig = plt.figure(figsize=(8, 6)); gs = fig.add_gridspec(2, 3) creates a 2x3 grid. ax_main = fig.add_subplot(gs[0, :2]) creates a panel spanning the first row, first two columns. ax_side = fig.add_subplot(gs[0, 2]) uses the remaining cell. This allows layouts like a large main plot with a smaller inset, or a wide panel above two narrow panels below, common for showing an overview alongside detailed comparisons.

Inset axes place a small plot inside a larger one. ax_inset = ax.inset_axes([0.6, 0.6, 0.35, 0.35]) creates an inset at 60% from the left, 60% from the bottom, with width and height 35% of the parent axes. Insets are useful for showing a zoomed-in view of a crowded region, displaying a different representation of the same data (like a log-scale inset of a linear-scale main plot), or adding a small overview map or context panel. The inset is a full Axes object with all the same customization capabilities as any other panel.

Step 4: Add Annotations and Special Elements

Text annotations label specific features. ax.annotate('Peak', xy=(x_peak, y_peak), xytext=(x_peak + 1, y_peak + 5), arrowprops=dict(arrowstyle='->', color='black'), fontsize=9) places the label "Peak" with an arrow pointing to the peak location. ax.text(0.05, 0.95, 'n = 150', transform=ax.transAxes, fontsize=8, verticalalignment='top') places text at 5% from the left and 95% from the bottom in axes coordinates (independent of data coordinates), standard for sample size labels and panel identifiers.

Error representation goes beyond simple error bars. ax.fill_between(x, y - std, y + std, alpha=0.2) creates a shaded confidence band around a line. ax.violinplot(data_groups) shows full distributions as smoothed density shapes. ax.boxplot(data_groups) shows medians, quartiles, and outliers. For paired comparisons, add significance brackets using ax.plot() and ax.text() to draw a horizontal line between groups with an asterisk indicating significance level.

Colorbars show the mapping between colors and values for scatter plots, heatmaps, and contour plots. After creating a scatter plot with color mapping (sc = ax.scatter(x, y, c=z, cmap='viridis')), call plt.colorbar(sc, ax=ax, label='Value'). For heatmaps, ax.imshow(data, cmap='coolwarm', aspect='auto') displays a 2D array as a colored grid, and plt.colorbar(label='Intensity') adds the scale. Set vmin and vmax to control the color range explicitly rather than having it auto-scale to the data range.

Statistical plots combine multiple elements. A regression plot might include data points (scatter), a fitted line (plot), confidence interval (fill_between), residuals (stem plot in an inset or lower panel), and annotation text showing the equation, R-squared, and p-value. Building these composite visualizations requires the object-oriented interface because you need precise control over which elements go on which axes and how they interact visually.

Step 5: Export Publication-Ready Figures

fig.savefig('figure.pdf', dpi=300, bbox_inches='tight') saves as PDF, the preferred format for journal submission because it is vector-based (infinitely scalable without pixelation). bbox_inches='tight' trims whitespace around the figure. dpi=300 sets the resolution for any rasterized elements (though pure vector elements in PDFs are resolution-independent). fig.savefig('figure.svg') saves as SVG, another vector format preferred for web display and presentations. fig.savefig('figure.png', dpi=300) saves as PNG at 300 DPI, suitable when a raster format is required.

Figure dimensions must match journal requirements. Most journals specify column widths: Nature uses 89 mm single-column and 183 mm double-column, Science uses 3.5 inches single and 7.25 inches double. Set figsize in inches when creating the figure: fig, ax = plt.subplots(figsize=(3.5, 2.5)) for a single-column figure. All text sizes should be legible at the final printed size: 8 to 10 pt for axis labels, 6 to 8 pt for tick labels, 10 to 12 pt for titles. Test by opening the saved PDF at 100% zoom and confirming readability.

Global style settings with rcParams ensure consistency across all figures in a project. plt.rcParams.update({'font.size': 9, 'axes.labelsize': 10, 'xtick.labelsize': 8, 'ytick.labelsize': 8, 'legend.fontsize': 8, 'figure.figsize': (3.5, 2.5), 'figure.dpi': 300, 'savefig.dpi': 300, 'font.family': 'sans-serif'}) sets defaults once. Alternatively, create a custom matplotlib style file (.mplstyle) with these settings and activate it with plt.style.use('mystyle'). This ensures that every figure in a paper uses the same fonts, sizes, and proportions without repeating configuration code in every script.

plt.tight_layout() adjusts subplot spacing to prevent overlapping labels. Call it after all plot elements are added and before savefig. fig.align_ylabels() aligns y-axis labels across rows of subplots. For complex layouts where tight_layout cannot resolve all overlaps, use fig.subplots_adjust(hspace=0.3, wspace=0.25) to manually set the horizontal and vertical spacing between panels, or use constrained_layout=True as a parameter to plt.subplots() for automatic spacing that handles most edge cases.

Key Takeaway

Use the object-oriented interface (fig, ax = plt.subplots()), set figure dimensions to match your journal's column width from the start, and save as PDF for publication. These three habits produce figures that require minimal revision.