Matplotlib Mastery: Basics to Advanced

A practical, in-depth guide to Python visualization

Python
Data Visualization
Matplotlib
Tutorial
Author

Rishabh Mondal

Published

February 15, 2026

Welcome!

This comprehensive guide teaches Matplotlib in a Question → Answer → Example format, organized into progressive levels:

  • Level 1 (Fundamentals): Core plotting, labels, styling, and saving figures
  • Level 2 (Intermediate): Subplots, chart selection, annotations, and statistical plots
  • Level 3 (Advanced): OO API, 3D plots, animations, colormaps, and professional techniques
  • Level 4 (Expert): Custom projections, advanced layouts, and production-ready code

Use this as a comprehensive reference, interview preparation guide, or project companion.


Learning Roadmap

Level Focus Outcome
Fundamentals Line plots, bar charts, labels, grid, saving Create clean, publication-ready basic charts
Intermediate Subplots, scatter, histograms, pie charts, annotations Build multi-panel figures with clear insights
Advanced OO API, 3D plots, colormaps, box/violin plots, heatmaps Design professional dashboards and complex visualizations
Expert GridSpec, animations, custom styles, transforms Create production-grade, reusable plotting systems

Part 1: Fundamentals

Q1. What is Matplotlib and why is it essential for data visualization?

Answer: Matplotlib is Python’s foundational 2D plotting library, created by John Hunter in 2003. It provides:

  • Complete control over every plot element (axes, ticks, labels, colors)
  • Publication-quality output in multiple formats (PNG, PDF, SVG, EPS)
  • Integration with NumPy, Pandas, and the entire scientific Python ecosystem
  • Two interfaces: quick pyplot (MATLAB-style) and powerful object-oriented API
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

print(f"Matplotlib version: {mpl.__version__}")
print(f"Available backends: {mpl.rcsetup.all_backends[:5]}...")
Matplotlib version: 3.7.5
Available backends: ['GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', 'MacOSX']...
Note

If Matplotlib is missing, install it with:

pip install matplotlib

Q2. What are the key parts of a Matplotlib figure?

Answer: Every Matplotlib visualization consists of:

  • Figure: The entire window/page containing everything
  • Axes: The actual plot area with data, ticks, labels (a Figure can have multiple Axes)
  • Axis: The x-axis and y-axis objects controlling ticks and limits
  • Artist: Everything visible on the figure (lines, text, patches, etc.)
fig, ax = plt.subplots(figsize=(8, 5))

# Demonstrate figure anatomy
ax.set_title("Figure Anatomy", fontsize=14, fontweight="bold")
ax.set_xlabel("X-axis (horizontal)")
ax.set_ylabel("Y-axis (vertical)")
ax.text(0.5, 0.5, "This is the Axes\n(plot area)", ha="center", va="center", 
        fontsize=12, bbox=dict(boxstyle="round", facecolor="lightblue"))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.grid(alpha=0.3)

# Add annotations showing parts
ax.annotate("Title", xy=(0.5, 1.02), xycoords="axes fraction", ha="center", fontsize=10, color="red")
ax.annotate("Spines", xy=(0, 0.5), xycoords="axes fraction", ha="right", fontsize=9, color="green")
fig.text(0.02, 0.5, "Figure boundary →", rotation=90, va="center", fontsize=9, color="purple")

plt.tight_layout()
plt.show()

Q3. What is the difference between plt (pyplot) and the object-oriented API?

Answer:

Aspect pyplot (plt) Object-Oriented (OO)
Style MATLAB-like, stateful Pythonic, explicit
Use case Quick plots, exploration Complex figures, automation
Control Limited Full control over every element
Reusability Harder to reuse Easy to create functions
# pyplot style (quick but less control)
plt.figure(figsize=(6, 3))
plt.plot([1, 2, 3], [1, 4, 9])
plt.title("Pyplot Style")
plt.show()

# Object-oriented style (recommended for complex work)
fig, ax = plt.subplots(figsize=(6, 3))
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_title("Object-Oriented Style")
plt.show()

Q4. How do I create my first line plot?

Answer: Use plt.plot() with x and y arrays, then add labels and show.

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

plt.figure(figsize=(8, 4))
plt.plot(x, y, color="royalblue", linewidth=2)
plt.title("Sine Wave", fontsize=14)
plt.xlabel("x")
plt.ylabel("sin(x)")
plt.grid(alpha=0.3)
plt.show()

Q5. What line styles, markers, and colors are available?

Answer: Matplotlib offers extensive customization:

Line styles: - (solid), -- (dashed), -. (dash-dot), : (dotted)

Markers: o (circle), s (square), ^ (triangle), D (diamond), * (star), +, x

Colors: Named colors, hex codes, RGB tuples, or color cycle shortcuts (C0, C1, etc.)

fig, axs = plt.subplots(1, 3, figsize=(14, 4))
x = np.linspace(0, 2, 20)

# Line styles
for i, ls in enumerate(['-', '--', '-.', ':']):
    axs[0].plot(x, x + i*0.5, linestyle=ls, linewidth=2, label=f"'{ls}'")
axs[0].set_title("Line Styles")
axs[0].legend()
axs[0].grid(alpha=0.3)

# Markers
markers = ['o', 's', '^', 'D', '*', 'p', 'h']
for i, m in enumerate(markers):
    axs[1].plot(x[::2], (x[::2] + i*0.3), marker=m, linestyle='-', markersize=8, label=f"'{m}'")
axs[1].set_title("Markers")
axs[1].legend(ncol=2, fontsize=8)
axs[1].grid(alpha=0.3)

# Colors
colors = ['#e63946', '#457b9d', '#2a9d8f', '#f4a261', '#264653']
for i, c in enumerate(colors):
    axs[2].bar(i, i+1, color=c, label=c)
axs[2].set_title("Custom Colors (Hex)")
axs[2].legend(fontsize=8)

plt.tight_layout()
plt.show()

Q6. How do I add multiple lines with a legend?

Answer: Plot each line with a label parameter, then call plt.legend().

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

plt.figure(figsize=(9, 4))
plt.plot(t, np.sin(t), label="sin(t)", linewidth=2)
plt.plot(t, np.cos(t), label="cos(t)", linewidth=2, linestyle="--")
plt.plot(t, np.sin(t) * np.cos(t), label="sin(t)·cos(t)", linewidth=2, linestyle="-.")
plt.title("Trigonometric Functions Comparison")
plt.xlabel("t (radians)")
plt.ylabel("Value")
plt.legend(loc="upper right")
plt.grid(alpha=0.25)
plt.axhline(y=0, color='k', linewidth=0.5)
plt.show()

Q7. How do I customize axis limits and ticks?

Answer: Use xlim(), ylim(), xticks(), yticks() or their OO equivalents.

x = np.linspace(0, 10, 100)
y = np.exp(-x/3) * np.sin(2*x)

fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(x, y, color="#2a9d8f", linewidth=2)

# Custom limits
ax.set_xlim(0, 8)
ax.set_ylim(-0.6, 1.0)

# Custom ticks
ax.set_xticks([0, 2, 4, 6, 8])
ax.set_xticklabels(['0', '2π/5', '4π/5', '6π/5', '8π/5'])
ax.set_yticks([-0.5, 0, 0.5, 1.0])

ax.set_title("Custom Axis Limits and Tick Labels")
ax.set_xlabel("Phase")
ax.set_ylabel("Amplitude")
ax.grid(alpha=0.3)
plt.show()

Q8. How do I save figures in different formats?

Answer: Use savefig() with the desired file extension. Matplotlib detects format from filename.

fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(np.random.randn(50).cumsum(), color="steelblue", linewidth=2)
ax.set_title("Random Walk")
ax.grid(alpha=0.3)

# Save in multiple formats (uncomment to save)
# fig.savefig("plot.png", dpi=300, bbox_inches="tight")  # Raster, web-friendly
# fig.savefig("plot.pdf", bbox_inches="tight")           # Vector, publication
# fig.savefig("plot.svg", bbox_inches="tight")           # Vector, scalable
# fig.savefig("plot.eps", bbox_inches="tight")           # Vector, LaTeX compatible

plt.show()
print("Supported formats:", fig.canvas.get_supported_filetypes())

Supported formats: {'eps': 'Encapsulated Postscript', 'jpg': 'Joint Photographic Experts Group', 'jpeg': 'Joint Photographic Experts Group', 'pdf': 'Portable Document Format', 'pgf': 'PGF code for LaTeX', 'png': 'Portable Network Graphics', 'ps': 'Postscript', 'raw': 'Raw RGBA bitmap', 'rgba': 'Raw RGBA bitmap', 'svg': 'Scalable Vector Graphics', 'svgz': 'Scalable Vector Graphics', 'tif': 'Tagged Image File Format', 'tiff': 'Tagged Image File Format', 'webp': 'WebP Image Format'}

Q9. How do I add grid lines selectively?

Answer: Use grid() with axis, which, and linestyle parameters.

fig, axs = plt.subplots(1, 3, figsize=(14, 4))
x = np.linspace(0, 10, 100)
y = np.sin(x)

# Default grid
axs[0].plot(x, y)
axs[0].grid(True)
axs[0].set_title("Default Grid")

# Only horizontal grid
axs[1].plot(x, y)
axs[1].grid(axis='y', alpha=0.5)
axs[1].set_title("Horizontal Only")

# Major and minor grid
axs[2].plot(x, y)
axs[2].grid(which='major', linestyle='-', alpha=0.7)
axs[2].grid(which='minor', linestyle=':', alpha=0.4)
axs[2].minorticks_on()
axs[2].set_title("Major + Minor Grid")

plt.tight_layout()
plt.show()

Q10. How do I add horizontal and vertical reference lines?

Answer: Use axhline(), axvline(), hlines(), and vlines().

fig, ax = plt.subplots(figsize=(9, 5))
x = np.linspace(0, 10, 100)
y = np.sin(x)

ax.plot(x, y, linewidth=2, label="sin(x)")

# Reference lines
ax.axhline(y=0, color='k', linestyle='-', linewidth=0.8, label="y=0")
ax.axhline(y=0.5, color='red', linestyle='--', alpha=0.7, label="y=0.5 threshold")
ax.axvline(x=np.pi, color='green', linestyle=':', linewidth=2, label="x=π")

# Span regions
ax.axhspan(-0.2, 0.2, alpha=0.2, color='yellow', label="Safe zone")
ax.axvspan(6, 8, alpha=0.15, color='blue', label="Highlight region")

ax.set_xlim(0, 10)
ax.legend(loc="upper right", fontsize=9)
ax.set_title("Reference Lines and Spans")
ax.grid(alpha=0.3)
plt.show()

Challenge 1 (Easy): Basic Line Plot

Create a simple line plot of \(y = x^2\) for x ∈ [0, 10].

Requirements: - Blue solid line with linewidth=2 - Title: “Quadratic Function” - X and Y axis labels - Grid enabled


Challenge 2 (Easy): Multiple Lines with Legend

Plot \(y = x\), \(y = x^2\), and \(y = x^3\) on the same figure for x ∈ [0, 3].

Requirements: - Different colors for each line - Line labels and legend - Add grid with alpha=0.3


Challenge 3 (Moderate): Customized Sine Waves

Create a figure showing \(\sin(x)\), \(\sin(2x)\), and \(\sin(3x)\) for x ∈ [0, 2π].

Requirements: - Different line styles (solid, dashed, dotted) - Custom colors (not default) - Legend positioned at ‘upper right’ - Add horizontal line at y=0 - Custom x-ticks at [0, π/2, π, 3π/2, 2π] with labels [‘0’, ‘π/2’, ‘π’, ‘3π/2’, ‘2π’]


Challenge 4 (Moderate): Reference Lines and Spans

Plot \(y = e^{-x} \cdot \cos(2\pi x)\) for x ∈ [0, 5].

Requirements: - Highlight the region where |y| < 0.2 using axhspan - Add vertical lines at x = 1, 2, 3, 4 using axvline - Mark the envelope curves \(\pm e^{-x}\) with dashed lines - Proper annotations explaining what each element represents


Challenge 5 (Difficult): Publication-Ready Multi-Curve Plot

Create a plot showing three exponential decay curves: \(e^{-x}\), \(e^{-2x}\), and \(e^{-0.5x}\) for x ∈ [0, 5].

Requirements: 1. Different line styles and colors for each curve 2. A horizontal line at y=0.1 (threshold) 3. Vertical line where \(e^{-x}\) crosses the threshold (x = ln(10) ≈ 2.303) 4. Annotation pointing to the intersection 5. LaTeX labels: \(e^{-x}\), \(e^{-2x}\), \(e^{-0.5x}\) 6. Legend with title “Decay Rates” 7. Save as both PNG (300 DPI) and SVG


Part 2: Intermediate Visualizations

Q11. How do I create bar charts (vertical and horizontal)?

Answer: Use bar() for vertical and barh() for horizontal bars.

categories = ['Python', 'JavaScript', 'Java', 'C++', 'Go']
values = [85, 72, 68, 45, 52]
colors = ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51']

fig, axs = plt.subplots(1, 2, figsize=(12, 4))

# Vertical bars
axs[0].bar(categories, values, color=colors, edgecolor='white', linewidth=1.5)
axs[0].set_title("Programming Language Popularity")
axs[0].set_ylabel("Score")
axs[0].set_ylim(0, 100)

# Add value labels on bars
for i, (cat, val) in enumerate(zip(categories, values)):
    axs[0].text(i, val + 2, str(val), ha='center', fontweight='bold')

# Horizontal bars
axs[1].barh(categories, values, color=colors, edgecolor='white', linewidth=1.5)
axs[1].set_title("Horizontal Bar Chart")
axs[1].set_xlabel("Score")
axs[1].set_xlim(0, 100)
axs[1].invert_yaxis()  # Top category first

plt.tight_layout()
plt.show()

Q12. How do I create grouped and stacked bar charts?

Answer: For grouped bars, offset the x positions. For stacked, use the bottom parameter.

categories = ['Q1', 'Q2', 'Q3', 'Q4']
product_a = [20, 35, 30, 35]
product_b = [25, 32, 34, 20]
product_c = [15, 25, 28, 30]

x = np.arange(len(categories))
width = 0.25

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

# Grouped bars
bars1 = axs[0].bar(x - width, product_a, width, label='Product A', color='#264653')
bars2 = axs[0].bar(x, product_b, width, label='Product B', color='#2a9d8f')
bars3 = axs[0].bar(x + width, product_c, width, label='Product C', color='#e9c46a')
axs[0].set_xlabel('Quarter')
axs[0].set_ylabel('Sales')
axs[0].set_title('Grouped Bar Chart: Quarterly Sales')
axs[0].set_xticks(x)
axs[0].set_xticklabels(categories)
axs[0].legend()
axs[0].grid(axis='y', alpha=0.3)

# Stacked bars
axs[1].bar(categories, product_a, label='Product A', color='#264653')
axs[1].bar(categories, product_b, bottom=product_a, label='Product B', color='#2a9d8f')
axs[1].bar(categories, product_c, bottom=np.array(product_a)+np.array(product_b), 
           label='Product C', color='#e9c46a')
axs[1].set_xlabel('Quarter')
axs[1].set_ylabel('Total Sales')
axs[1].set_title('Stacked Bar Chart: Quarterly Sales')
axs[1].legend()
axs[1].grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

Q13. How do I create scatter plots with varying size and color?

Answer: Use the s (size) and c (color) parameters with colormaps.

np.random.seed(42)
n = 100
x = np.random.randn(n)
y = x + np.random.randn(n) * 0.5
colors = np.random.rand(n)
sizes = np.abs(np.random.randn(n)) * 200 + 50

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

# Basic scatter
axs[0].scatter(x, y, alpha=0.7, edgecolors='white', linewidth=0.5)
axs[0].set_title("Basic Scatter Plot")
axs[0].set_xlabel("X")
axs[0].set_ylabel("Y")
axs[0].grid(alpha=0.3)

# Scatter with size and color mapping
scatter = axs[1].scatter(x, y, c=colors, s=sizes, cmap='viridis', 
                          alpha=0.7, edgecolors='white', linewidth=0.5)
axs[1].set_title("Scatter with Color & Size Mapping")
axs[1].set_xlabel("X")
axs[1].set_ylabel("Y")
axs[1].grid(alpha=0.3)
plt.colorbar(scatter, ax=axs[1], label="Color Value")

plt.tight_layout()
plt.show()

Q14. How do I create histograms with different configurations?

Answer: Use hist() with parameters like bins, density, histtype, alpha.

np.random.seed(42)
data1 = np.random.normal(0, 1, 1000)
data2 = np.random.normal(2, 1.5, 1000)

fig, axs = plt.subplots(2, 2, figsize=(12, 9))

# Basic histogram
axs[0, 0].hist(data1, bins=30, color='steelblue', edgecolor='white', alpha=0.8)
axs[0, 0].set_title("Basic Histogram")
axs[0, 0].set_xlabel("Value")
axs[0, 0].set_ylabel("Frequency")

# Density histogram (normalized)
axs[0, 1].hist(data1, bins=30, density=True, color='#2a9d8f', edgecolor='white', alpha=0.8)
axs[0, 1].set_title("Density Histogram (Normalized)")
axs[0, 1].set_xlabel("Value")
axs[0, 1].set_ylabel("Density")

# Overlapping histograms
axs[1, 0].hist(data1, bins=30, alpha=0.6, label='Distribution 1', color='#264653')
axs[1, 0].hist(data2, bins=30, alpha=0.6, label='Distribution 2', color='#e76f51')
axs[1, 0].set_title("Overlapping Histograms")
axs[1, 0].legend()

# Step histogram
axs[1, 1].hist(data1, bins=30, histtype='step', linewidth=2, label='Step', color='#264653')
axs[1, 1].hist(data1, bins=30, histtype='stepfilled', alpha=0.3, label='Filled', color='#264653')
axs[1, 1].set_title("Step Histogram Types")
axs[1, 1].legend()

for ax in axs.flat:
    ax.grid(alpha=0.3)

plt.tight_layout()
plt.show()

Q15. How do I create pie charts and donut charts?

Answer: Use pie() with explode for emphasis and a white center circle for donuts.

labels = ['Python', 'JavaScript', 'Java', 'C++', 'Others']
sizes = [35, 25, 20, 12, 8]
colors = ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51']
explode = (0.05, 0, 0, 0, 0)  # Explode Python slice

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

# Basic pie chart
axs[0].pie(sizes, explode=explode, labels=labels, colors=colors, autopct='%1.1f%%',
           shadow=False, startangle=90, textprops={'fontsize': 10})
axs[0].set_title("Pie Chart: Language Usage", fontsize=12)

# Donut chart
wedges, texts, autotexts = axs[1].pie(sizes, colors=colors, autopct='%1.1f%%',
                                        startangle=90, pctdistance=0.75,
                                        textprops={'fontsize': 9, 'color': 'white'})
# Create donut by adding white circle
centre_circle = plt.Circle((0, 0), 0.50, fc='white')
axs[1].add_patch(centre_circle)
axs[1].legend(wedges, labels, title="Languages", loc="center left", bbox_to_anchor=(0.9, 0.5))
axs[1].set_title("Donut Chart: Language Usage", fontsize=12)

plt.tight_layout()
plt.show()

Q16. How do I create subplots with plt.subplots()?

Answer: subplots(nrows, ncols) returns a figure and array of axes.

fig, axs = plt.subplots(2, 3, figsize=(14, 8))
x = np.linspace(0, 2*np.pi, 100)

# Different plots in each subplot
axs[0, 0].plot(x, np.sin(x), 'b-')
axs[0, 0].set_title("sin(x)")

axs[0, 1].plot(x, np.cos(x), 'r--')
axs[0, 1].set_title("cos(x)")

axs[0, 2].plot(x, np.tan(x), 'g-.')
axs[0, 2].set_ylim(-5, 5)
axs[0, 2].set_title("tan(x) (clipped)")

axs[1, 0].bar(['A', 'B', 'C'], [3, 7, 5], color=['#264653', '#2a9d8f', '#e9c46a'])
axs[1, 0].set_title("Bar Chart")

axs[1, 1].scatter(np.random.rand(30), np.random.rand(30), c=np.random.rand(30), cmap='viridis')
axs[1, 1].set_title("Scatter Plot")

axs[1, 2].hist(np.random.randn(200), bins=15, color='steelblue', edgecolor='white')
axs[1, 2].set_title("Histogram")

# Add super title and common labels
fig.suptitle("Multiple Subplot Demonstration", fontsize=14, fontweight='bold')
fig.supxlabel("X-axis")
fig.supylabel("Y-axis")

plt.tight_layout()
plt.show()

Q17. How do I share axes between subplots?

Answer: Use sharex=True or sharey=True parameters.

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

np.random.seed(42)
data = [np.random.randn(100) * scale + mean for scale, mean in [(1, 0), (1.5, 2), (0.5, -1), (2, 1)]]

for i, ax in enumerate(axs.flat):
    ax.hist(data[i], bins=20, alpha=0.7, color=f'C{i}')
    ax.set_title(f"Distribution {i+1}")

# Only outer axes have labels due to sharing
for ax in axs[-1, :]:
    ax.set_xlabel("Value")
for ax in axs[:, 0]:
    ax.set_ylabel("Frequency")

fig.suptitle("Shared Axes: Columns share X, Rows share Y", fontsize=12)
plt.tight_layout()
plt.show()

Q18. How do I add text and annotations to plots?

Answer: Use text() for static text and annotate() for text with arrows.

fig, ax = plt.subplots(figsize=(10, 6))
x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x/10)

ax.plot(x, y, 'b-', linewidth=2)

# Find maximum
max_idx = np.argmax(y)
max_x, max_y = x[max_idx], y[max_idx]

# Annotation with arrow
ax.annotate(f'Maximum\n({max_x:.2f}, {max_y:.2f})',
            xy=(max_x, max_y),
            xytext=(max_x + 2, max_y + 0.3),
            fontsize=10,
            arrowprops=dict(arrowstyle='->', color='red', lw=1.5),
            bbox=dict(boxstyle='round,pad=0.3', facecolor='lightyellow', edgecolor='orange'))

# Simple text
ax.text(7, 0.2, "Damped oscillation", fontsize=11, style='italic', 
        bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.5))

# Mathematical text using LaTeX
ax.text(5, -0.4, r'$y = \sin(x) \cdot e^{-x/10}$', fontsize=12)

ax.set_title("Annotations and Text Placement")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(alpha=0.3)
ax.axhline(y=0, color='k', linewidth=0.5)
plt.show()

Q19. How do I create error bars for uncertainty visualization?

Answer: Use errorbar() with xerr and/or yerr parameters.

# Experimental data with uncertainties
x = np.array([1, 2, 3, 4, 5])
y = np.array([2.1, 3.9, 6.2, 7.8, 10.1])
y_err = np.array([0.3, 0.4, 0.5, 0.3, 0.6])
x_err = np.array([0.1, 0.1, 0.15, 0.1, 0.2])

fig, axs = plt.subplots(1, 3, figsize=(14, 4))

# Symmetric y-error bars
axs[0].errorbar(x, y, yerr=y_err, fmt='o-', capsize=4, capthick=2, 
                color='#264653', ecolor='#e76f51', markersize=8)
axs[0].set_title("Y Error Bars")
axs[0].grid(alpha=0.3)

# Both x and y error bars
axs[1].errorbar(x, y, xerr=x_err, yerr=y_err, fmt='s', capsize=3, 
                color='#2a9d8f', ecolor='gray', markersize=8)
axs[1].set_title("X and Y Error Bars")
axs[1].grid(alpha=0.3)

# Asymmetric error bars
y_err_asym = np.array([[0.2, 0.3, 0.2, 0.25, 0.3],  # Lower errors
                       [0.4, 0.5, 0.6, 0.35, 0.7]])  # Upper errors
axs[2].errorbar(x, y, yerr=y_err_asym, fmt='D-', capsize=4, 
                color='#e9c46a', ecolor='#264653', markersize=8)
axs[2].set_title("Asymmetric Error Bars")
axs[2].grid(alpha=0.3)

for ax in axs:
    ax.set_xlabel("X")
    ax.set_ylabel("Y")

plt.tight_layout()
plt.show()

Q20. How do I create box plots for statistical distribution comparison?

Answer: Use boxplot() or the more flexible bxp().

np.random.seed(42)
data = [np.random.normal(0, std, 100) for std in [1, 1.5, 2, 0.8]]
labels = ['Group A', 'Group B', 'Group C', 'Group D']

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

# Basic box plot
bp = axs[0].boxplot(data, labels=labels, patch_artist=True)
colors = ['#264653', '#2a9d8f', '#e9c46a', '#e76f51']
for patch, color in zip(bp['boxes'], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.7)
axs[0].set_title("Box Plot: Distribution Comparison")
axs[0].set_ylabel("Value")
axs[0].grid(axis='y', alpha=0.3)

# Horizontal box plot with notch
bp2 = axs[1].boxplot(data, labels=labels, patch_artist=True, vert=False, notch=True)
for patch, color in zip(bp2['boxes'], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.7)
axs[1].set_title("Horizontal Notched Box Plot")
axs[1].set_xlabel("Value")
axs[1].grid(axis='x', alpha=0.3)

plt.tight_layout()
plt.show()

Q21. How do I create violin plots for richer distribution visualization?

Answer: Use violinplot() to show both the distribution shape and summary statistics.

np.random.seed(42)
data = [np.concatenate([np.random.normal(0, 1, 50), np.random.normal(3, 0.5, 30)]),
        np.random.normal(1, 1.5, 100),
        np.random.exponential(1, 100),
        np.random.uniform(-2, 2, 100)]

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

# Basic violin plot
vp = axs[0].violinplot(data, showmeans=True, showmedians=True)
for i, body in enumerate(vp['bodies']):
    body.set_facecolor(f'C{i}')
    body.set_alpha(0.7)
axs[0].set_xticks([1, 2, 3, 4])
axs[0].set_xticklabels(['Bimodal', 'Normal', 'Exponential', 'Uniform'])
axs[0].set_title("Violin Plot: Different Distributions")
axs[0].set_ylabel("Value")
axs[0].grid(alpha=0.3)

# Half violin with box plot overlay
parts = axs[1].violinplot(data, showextrema=False)
for i, body in enumerate(parts['bodies']):
    body.set_facecolor(f'C{i}')
    body.set_alpha(0.7)
# Add box plots on top
bp = axs[1].boxplot(data, widths=0.15)
axs[1].set_xticks([1, 2, 3, 4])
axs[1].set_xticklabels(['Bimodal', 'Normal', 'Exponential', 'Uniform'])
axs[1].set_title("Violin + Box Plot Overlay")
axs[1].set_ylabel("Value")
axs[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

Q22. What chart type should I use for different data scenarios?

Answer: Chart selection depends on your data and the story you want to tell:

Data Type Chart Use Case
Trend over time Line Time series, continuous change
Category comparison Bar Discrete categories, rankings
Relationship Scatter Correlation, clusters
Distribution Histogram, Box, Violin Spread, outliers, shape
Part of whole Pie, Stacked Bar Proportions (use sparingly)
2D density Heatmap, Contour Matrix data, correlations
Hierarchical Treemap, Sunburst Nested categories
fig, axs = plt.subplots(2, 3, figsize=(14, 9))
np.random.seed(42)

# Time series → Line
dates = np.arange(30)
values = np.cumsum(np.random.randn(30)) + 50
axs[0, 0].plot(dates, values, 'o-', color='#264653', markersize=4)
axs[0, 0].set_title("Trend → Line Chart")
axs[0, 0].set_xlabel("Day")
axs[0, 0].fill_between(dates, values, alpha=0.2)

# Categories → Bar
cats = ['A', 'B', 'C', 'D']
vals = [25, 40, 30, 45]
axs[0, 1].bar(cats, vals, color=['#264653', '#2a9d8f', '#e9c46a', '#e76f51'])
axs[0, 1].set_title("Comparison → Bar Chart")

# Correlation → Scatter
x = np.random.randn(50)
y = 0.7*x + np.random.randn(50)*0.5
axs[0, 2].scatter(x, y, alpha=0.7, c='#2a9d8f', edgecolors='white')
axs[0, 2].set_title("Relationship → Scatter")

# Distribution → Histogram
data = np.random.randn(500)
axs[1, 0].hist(data, bins=25, color='#264653', edgecolor='white', alpha=0.8)
axs[1, 0].set_title("Distribution → Histogram")

# Spread → Box plot
data = [np.random.randn(100)*s + m for s, m in [(1, 0), (0.5, 2), (1.5, 1)]]
axs[1, 1].boxplot(data, labels=['Low var', 'High mean', 'High var'], patch_artist=True)
axs[1, 1].set_title("Spread → Box Plot")

# Proportion → Pie (use sparingly!)
sizes = [35, 25, 20, 20]
axs[1, 2].pie(sizes, labels=['A', 'B', 'C', 'D'], autopct='%1.0f%%', 
              colors=['#264653', '#2a9d8f', '#e9c46a', '#e76f51'])
axs[1, 2].set_title("Proportion → Pie (careful!)")

for ax in axs.flat:
    if ax != axs[1, 2]:
        ax.grid(alpha=0.3)

plt.tight_layout()
plt.show()

Challenge 1 (Easy): Basic Bar Chart

Create a bar chart comparing sales across 5 products: [‘Laptop’, ‘Phone’, ‘Tablet’, ‘Watch’, ‘Earbuds’] with values [150, 280, 95, 120, 200].

Requirements: - Different color for each bar - Add value labels on top of each bar - Title and axis labels - Grid on y-axis only


Challenge 2 (Easy): Simple Scatter Plot

Generate 50 random points (x from uniform [0, 10], y = 2x + noise).

Requirements: - Scatter plot with alpha=0.7 - Different marker size based on y-value - Add a trend line (y = 2x) - Legend distinguishing data vs. trend


Challenge 3 (Moderate): Grouped Bar Chart with Error Bars

Compare performance metrics across 4 teams for 3 quarters.

Requirements: - Grouped bars (3 groups of 4 bars each) - Error bars representing standard deviation - Different colors per quarter - Rotated x-tick labels - Legend outside the plot area


Challenge 4 (Moderate): Histogram with Statistical Annotations

Generate 1000 samples from a normal distribution (mean=75, std=10).

Requirements: - Histogram with 30 bins - Overlay a density curve (kernel density estimate or theoretical normal) - Vertical lines showing mean and ±1σ, ±2σ - Text annotation showing mean and standard deviation values - Different colors for each region (within 1σ, between 1σ-2σ, beyond 2σ)


Challenge 5 (Difficult): Comprehensive Analytics Dashboard

Create a 2×2 subplot panel analyzing mock business data:

Requirements: 1. Top-left: Monthly revenue time series (line chart with fill_between showing growth area) 2. Top-right: Revenue by product category (horizontal bar chart, sorted by value) 3. Bottom-left: Revenue vs. expenses scatter plot with: - Color representing profit margin - Size representing transaction volume - Colorbar showing profit scale 4. Bottom-right: Distribution of daily transactions (histogram with overlaid density curve)

Add: - Super title: “Business Analytics Dashboard” - Consistent color scheme across all panels - Proper annotations for key insights (e.g., best month, highest margin product)


Part 3: Advanced Visualizations

Q23. How do I use GridSpec for complex subplot layouts?

Answer: GridSpec provides fine-grained control over subplot sizes and arrangements.

import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(12, 8))
gs = gridspec.GridSpec(3, 3, figure=fig, width_ratios=[2, 1, 1], height_ratios=[1, 2, 1])

# Large plot on left spanning 2 rows
ax_main = fig.add_subplot(gs[0:2, 0])
x = np.linspace(0, 10, 100)
ax_main.plot(x, np.sin(x), label='sin(x)')
ax_main.plot(x, np.cos(x), label='cos(x)')
ax_main.set_title("Main Plot (2 rows)")
ax_main.legend()
ax_main.grid(alpha=0.3)

# Top right corner plots
ax_top1 = fig.add_subplot(gs[0, 1])
ax_top1.bar(['A', 'B', 'C'], [3, 7, 5], color=['#264653', '#2a9d8f', '#e9c46a'])
ax_top1.set_title("Top 1")

ax_top2 = fig.add_subplot(gs[0, 2])
ax_top2.scatter(np.random.rand(20), np.random.rand(20), c='#e76f51')
ax_top2.set_title("Top 2")

# Middle right
ax_mid = fig.add_subplot(gs[1, 1:])
ax_mid.hist(np.random.randn(200), bins=20, color='steelblue', edgecolor='white')
ax_mid.set_title("Middle Wide (spans 2 columns)")

# Bottom row spanning all columns
ax_bottom = fig.add_subplot(gs[2, :])
ax_bottom.plot(np.random.randn(100).cumsum(), color='#2a9d8f', linewidth=2)
ax_bottom.set_title("Bottom Wide (spans 3 columns)")
ax_bottom.grid(alpha=0.3)

plt.tight_layout()
plt.show()

Q24. How do I create heatmaps with imshow and pcolormesh?

Answer: Use imshow() for regular grids and pcolormesh() for more flexibility.

# Create sample data
np.random.seed(42)
data = np.random.rand(10, 10)
correlation = np.corrcoef(np.random.randn(5, 100))

fig, axs = plt.subplots(1, 3, figsize=(15, 4.5))

# Basic heatmap with imshow
im1 = axs[0].imshow(data, cmap='viridis', aspect='auto')
axs[0].set_title("imshow: Random Data")
plt.colorbar(im1, ax=axs[0], shrink=0.8)

# Correlation heatmap with annotations
im2 = axs[1].imshow(correlation, cmap='RdBu_r', vmin=-1, vmax=1)
for i in range(5):
    for j in range(5):
        axs[1].text(j, i, f'{correlation[i,j]:.2f}', ha='center', va='center', 
                   color='white' if abs(correlation[i,j]) > 0.5 else 'black', fontsize=9)
axs[1].set_title("Correlation Heatmap")
axs[1].set_xticks(range(5))
axs[1].set_yticks(range(5))
axs[1].set_xticklabels([f'Var{i+1}' for i in range(5)])
axs[1].set_yticklabels([f'Var{i+1}' for i in range(5)])
plt.colorbar(im2, ax=axs[1], shrink=0.8, label='Correlation')

# pcolormesh with custom grid
x = np.arange(0, 11, 1)
y = np.arange(0, 11, 1)
X, Y = np.meshgrid(x, y)
Z = np.sin(X/2) * np.cos(Y/2)
im3 = axs[2].pcolormesh(X, Y, Z, cmap='coolwarm', shading='auto')
axs[2].set_title("pcolormesh: sin(x)·cos(y)")
plt.colorbar(im3, ax=axs[2], shrink=0.8)

plt.tight_layout()
plt.show()

Q25. How do I create contour plots?

Answer: Use contour() for lines and contourf() for filled contours.

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) * np.exp(-0.1*(X**2 + Y**2))

fig, axs = plt.subplots(1, 3, figsize=(15, 4.5))

# Contour lines
cs1 = axs[0].contour(X, Y, Z, levels=15, cmap='viridis')
axs[0].clabel(cs1, inline=True, fontsize=8)
axs[0].set_title("Contour Lines")
axs[0].set_xlabel("X")
axs[0].set_ylabel("Y")

# Filled contours
cs2 = axs[1].contourf(X, Y, Z, levels=20, cmap='RdYlBu_r')
plt.colorbar(cs2, ax=axs[1])
axs[1].set_title("Filled Contours")
axs[1].set_xlabel("X")
axs[1].set_ylabel("Y")

# Contour lines over filled
cs3_fill = axs[2].contourf(X, Y, Z, levels=15, cmap='coolwarm', alpha=0.8)
cs3_line = axs[2].contour(X, Y, Z, levels=15, colors='black', linewidths=0.5)
axs[2].clabel(cs3_line, inline=True, fontsize=7)
plt.colorbar(cs3_fill, ax=axs[2])
axs[2].set_title("Filled + Line Overlay")
axs[2].set_xlabel("X")
axs[2].set_ylabel("Y")

plt.tight_layout()
plt.show()

Q26. How do I create 3D plots?

Answer: Import mpl_toolkits.mplot3d and use projection='3d'.

from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(15, 5))

# 3D Surface plot
ax1 = fig.add_subplot(131, projection='3d')
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))
surf = ax1.plot_surface(X, Y, Z, cmap='viridis', alpha=0.8, edgecolor='none')
ax1.set_title("3D Surface")
ax1.set_xlabel("X")
ax1.set_ylabel("Y")
ax1.set_zlabel("Z")

# 3D Wireframe
ax2 = fig.add_subplot(132, projection='3d')
ax2.plot_wireframe(X, Y, Z, color='steelblue', linewidth=0.5)
ax2.set_title("3D Wireframe")
ax2.set_xlabel("X")
ax2.set_ylabel("Y")

# 3D Scatter
ax3 = fig.add_subplot(133, projection='3d')
np.random.seed(42)
xs = np.random.randn(100)
ys = np.random.randn(100)
zs = xs + ys + np.random.randn(100) * 0.5
ax3.scatter(xs, ys, zs, c=zs, cmap='plasma', s=30, alpha=0.7)
ax3.set_title("3D Scatter")
ax3.set_xlabel("X")
ax3.set_ylabel("Y")
ax3.set_zlabel("Z")

plt.tight_layout()
plt.show()

Q27. How do I create 3D bar charts and line plots?

Answer: Use bar3d() for bars and plot() on 3D axes for lines.

fig = plt.figure(figsize=(14, 5))

# 3D Bar chart
ax1 = fig.add_subplot(121, projection='3d')
x = np.arange(4)
y = np.arange(3)
_x, _y = np.meshgrid(x, y)
x_flat, y_flat = _x.ravel(), _y.ravel()
z_flat = np.zeros_like(x_flat)
values = np.array([[3, 5, 7, 4], [2, 6, 4, 8], [5, 3, 6, 2]]).ravel()
colors = plt.cm.viridis(values / values.max())

ax1.bar3d(x_flat, y_flat, z_flat, 0.6, 0.6, values, color=colors, alpha=0.8)
ax1.set_xlabel("Product")
ax1.set_ylabel("Quarter")
ax1.set_zlabel("Sales")
ax1.set_title("3D Bar Chart")

# 3D Parametric curve
ax2 = fig.add_subplot(122, projection='3d')
t = np.linspace(0, 4*np.pi, 200)
x = np.sin(t)
y = np.cos(t)
z = t / (4*np.pi)
ax2.plot(x, y, z, linewidth=2, color='#e76f51')
ax2.scatter(x[::20], y[::20], z[::20], s=30, c='#264653')
ax2.set_title("3D Parametric Curve (Helix)")
ax2.set_xlabel("X")
ax2.set_ylabel("Y")
ax2.set_zlabel("Z")

plt.tight_layout()
plt.show()

Q28. How do I use dual y-axes effectively?

Answer: Use twinx() for a secondary y-axis with different scale.

fig, ax1 = plt.subplots(figsize=(10, 5))

# Primary axis - Temperature
months = np.arange(1, 13)
temperature = np.array([5, 7, 12, 18, 23, 28, 30, 29, 24, 17, 10, 6])
ax1.plot(months, temperature, 'o-', color='#e76f51', linewidth=2, markersize=8, label='Temperature')
ax1.set_xlabel("Month", fontsize=11)
ax1.set_ylabel("Temperature (°C)", color='#e76f51', fontsize=11)
ax1.tick_params(axis='y', labelcolor='#e76f51')
ax1.set_xticks(months)
ax1.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 
                     'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])

# Secondary axis - Rainfall
ax2 = ax1.twinx()
rainfall = np.array([45, 38, 42, 55, 70, 85, 60, 65, 75, 80, 55, 50])
ax2.bar(months, rainfall, alpha=0.4, color='#264653', label='Rainfall')
ax2.set_ylabel("Rainfall (mm)", color='#264653', fontsize=11)
ax2.tick_params(axis='y', labelcolor='#264653')

# Combined legend
lines_1, labels_1 = ax1.get_legend_handles_labels()
lines_2, labels_2 = ax2.get_legend_handles_labels()
ax1.legend(lines_1 + lines_2, labels_1 + labels_2, loc='upper right')

ax1.set_title("Monthly Temperature and Rainfall", fontsize=13, fontweight='bold')
ax1.grid(alpha=0.3)
plt.tight_layout()
plt.show()

Q29. How do I customize spines (axis lines)?

Answer: Access spines through ax.spines and modify visibility, position, and style.

fig, axs = plt.subplots(2, 2, figsize=(11, 9))
x = np.linspace(-2, 2, 100)
y = x**2

# Default spines
axs[0, 0].plot(x, y, 'b-', linewidth=2)
axs[0, 0].set_title("Default Spines")
axs[0, 0].grid(alpha=0.3)

# Centered spines (like mathematical axes)
axs[0, 1].plot(x, np.sin(x*np.pi), 'r-', linewidth=2)
axs[0, 1].spines['left'].set_position('center')
axs[0, 1].spines['bottom'].set_position('center')
axs[0, 1].spines['right'].set_color('none')
axs[0, 1].spines['top'].set_color('none')
axs[0, 1].set_title("Centered Spines")

# Only left and bottom (classic)
axs[1, 0].plot(x, y, 'g-', linewidth=2)
axs[1, 0].spines['right'].set_visible(False)
axs[1, 0].spines['top'].set_visible(False)
axs[1, 0].set_title("Left & Bottom Only")

# Box with thick spines
axs[1, 1].plot(x, y, 'm-', linewidth=2)
for spine in axs[1, 1].spines.values():
    spine.set_linewidth(2)
    spine.set_edgecolor('#264653')
axs[1, 1].set_title("Thick Colored Spines")

plt.tight_layout()
plt.show()

Q30. How do I work with colormaps and color normalization?

Answer: Matplotlib offers sequential, diverging, and categorical colormaps with various normalization options.

import matplotlib.colors as mcolors

fig, axs = plt.subplots(2, 3, figsize=(15, 9))
data = np.random.rand(10, 10)

# Different colormaps
cmaps = ['viridis', 'plasma', 'coolwarm', 'RdYlGn', 'Spectral', 'twilight']
for ax, cmap in zip(axs.flat, cmaps):
    im = ax.imshow(data, cmap=cmap)
    ax.set_title(f"cmap='{cmap}'")
    plt.colorbar(im, ax=ax, shrink=0.7)
    ax.set_xticks([])
    ax.set_yticks([])

plt.suptitle("Common Colormaps in Matplotlib", fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

Q31. How do I use logarithmic and other scales?

Answer: Use set_xscale() or set_yscale() with ‘log’, ‘symlog’, or ‘logit’.

fig, axs = plt.subplots(2, 2, figsize=(12, 9))
x = np.linspace(0.01, 100, 1000)
y = x**2

# Linear scale
axs[0, 0].plot(x, y)
axs[0, 0].set_title("Linear Scale")
axs[0, 0].grid(True, alpha=0.3)

# Logarithmic y-axis
axs[0, 1].plot(x, y)
axs[0, 1].set_yscale('log')
axs[0, 1].set_title("Log Y Scale")
axs[0, 1].grid(True, alpha=0.3, which='both')

# Log-log scale
axs[1, 0].plot(x, y)
axs[1, 0].set_xscale('log')
axs[1, 0].set_yscale('log')
axs[1, 0].set_title("Log-Log Scale")
axs[1, 0].grid(True, alpha=0.3, which='both')

# Symlog for data with negative values
x2 = np.linspace(-100, 100, 500)
y2 = x2
axs[1, 1].plot(x2, y2)
axs[1, 1].set_xscale('symlog', linthresh=10)
axs[1, 1].set_yscale('symlog', linthresh=10)
axs[1, 1].set_title("Symmetric Log (symlog)")
axs[1, 1].grid(True, alpha=0.3)
axs[1, 1].axhline(0, color='k', linewidth=0.5)
axs[1, 1].axvline(0, color='k', linewidth=0.5)

plt.tight_layout()
plt.show()

Q32. How do I create polar plots?

Answer: Use projection='polar' when creating axes.

fig, axs = plt.subplots(1, 3, figsize=(14, 4.5), subplot_kw={'projection': 'polar'})

# Polar line plot
theta = np.linspace(0, 2*np.pi, 100)
r = 1 + 0.5*np.cos(5*theta)
axs[0].plot(theta, r, linewidth=2, color='#2a9d8f')
axs[0].fill(theta, r, alpha=0.3, color='#2a9d8f')
axs[0].set_title("Polar Rose (r = 1 + 0.5cos(5θ))", pad=20)

# Polar bar chart (radar chart)
categories = ['Speed', 'Power', 'Defense', 'Magic', 'Luck', 'HP']
values = [85, 70, 60, 90, 75, 80]
angles = np.linspace(0, 2*np.pi, len(categories), endpoint=False).tolist()
values_closed = values + [values[0]]
angles_closed = angles + [angles[0]]
axs[1].plot(angles_closed, values_closed, 'o-', linewidth=2, color='#e76f51')
axs[1].fill(angles_closed, values_closed, alpha=0.3, color='#e76f51')
axs[1].set_xticks(angles)
axs[1].set_xticklabels(categories)
axs[1].set_title("Radar Chart: Character Stats", pad=20)

# Polar scatter
np.random.seed(42)
theta_scatter = np.random.uniform(0, 2*np.pi, 50)
r_scatter = np.random.uniform(0.5, 2, 50)
colors = np.random.rand(50)
axs[2].scatter(theta_scatter, r_scatter, c=colors, cmap='viridis', alpha=0.7, s=50)
axs[2].set_title("Polar Scatter", pad=20)

plt.tight_layout()
plt.show()

Q33. How do I add mathematical expressions using LaTeX?

Answer: Wrap text in raw strings with $...$ for inline or $$...$$ for display math.

fig, ax = plt.subplots(figsize=(10, 6))

x = np.linspace(-3, 3, 200)
y = (1/np.sqrt(2*np.pi)) * np.exp(-x**2/2)

ax.plot(x, y, linewidth=2.5, color='#264653', label=r'$f(x) = \frac{1}{\sqrt{2\pi}}e^{-\frac{x^2}{2}}$')
ax.fill_between(x, y, alpha=0.2, color='#264653')

# Add mathematical annotations
ax.text(0, 0.45, r'$\mu = 0, \sigma = 1$', fontsize=12, ha='center')
ax.text(1.5, 0.15, r'$\int_{-\infty}^{\infty} f(x)dx = 1$', fontsize=11, 
        bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))

# Annotate inflection points
ax.annotate(r'Inflection: $x = -\sigma$', xy=(-1, 0.24), xytext=(-2.5, 0.3),
            arrowprops=dict(arrowstyle='->', color='gray'), fontsize=10)
ax.annotate(r'Inflection: $x = +\sigma$', xy=(1, 0.24), xytext=(2, 0.35),
            arrowprops=dict(arrowstyle='->', color='gray'), fontsize=10)

ax.set_title(r'Standard Normal Distribution: $\mathcal{N}(0, 1)$', fontsize=14)
ax.set_xlabel(r'$x$', fontsize=12)
ax.set_ylabel(r'$f(x)$', fontsize=12)
ax.legend(loc='upper right', fontsize=11)
ax.grid(alpha=0.3)
ax.set_xlim(-3, 3)
ax.set_ylim(0, 0.5)
plt.tight_layout()
plt.show()

Q34. How do I use built-in style sheets?

Answer: Use plt.style.use() or plt.style.context() for different visual themes.

# Show available styles
print("Available styles:", plt.style.available[:10], "...")

# Compare styles
styles = ['default', 'seaborn-v0_8-whitegrid', 'ggplot', 'dark_background']
fig, axs = plt.subplots(2, 2, figsize=(12, 9))
x = np.linspace(0, 10, 50)

for ax, style in zip(axs.flat, styles):
    with plt.style.context(style):
        ax.plot(x, np.sin(x), label='sin(x)')
        ax.plot(x, np.cos(x), label='cos(x)')
        ax.set_title(f"Style: '{style}'", fontsize=10)
        ax.legend(fontsize=8)
        ax.grid(True)

plt.tight_layout()
plt.show()
Available styles: ['Solarize_Light2', '_classic_test_patch', '_mpl-gallery', '_mpl-gallery-nogrid', 'bmh', 'classic', 'dark_background', 'fast', 'fivethirtyeight', 'ggplot'] ...

Challenge 1 (Easy): Basic Heatmap

Create a 10×10 heatmap of random values.

Requirements: - Use ‘viridis’ colormap - Add colorbar with label - Row and column labels - Title


Challenge 2 (Easy): Simple 3D Plot

Create a 3D surface plot of \(z = \sin(\sqrt{x^2 + y^2})\) for x, y ∈ [-5, 5].

Requirements: - Use ‘coolwarm’ colormap - Set appropriate viewing angle - Label all three axes - Add title


Challenge 3 (Moderate): Dual-Axis Time Series

Plot temperature and humidity over 30 days.

Requirements: - Temperature on left y-axis (°C), humidity on right y-axis (%) - Different colors matching y-axis label colors - Combined legend - Shaded region highlighting days where both metrics exceeded thresholds - Grid aligned with primary axis


Challenge 4 (Moderate): Annotated Correlation Heatmap

Create a correlation matrix visualization for 6 variables.

Requirements: - Symmetric heatmap with diagonal of 1s - Diverging colormap centered at 0 (‘RdBu_r’) - Annotate each cell with correlation value - Text color: white for |r| > 0.5, black otherwise - Mask the upper triangle (show only lower triangle + diagonal) - Custom colorbar ticks at [-1, -0.5, 0, 0.5, 1]


Challenge 5 (Difficult): Multi-View Scientific Dashboard

Create a dashboard with GridSpec showing a 2D function \(z = \sin(x) \cdot \cos(y)\) from multiple perspectives:

Layout (using GridSpec): - Left (2 rows): 3D surface plot with custom colormap - Top-right: Heatmap (imshow) of the same function - Middle-right: Contour plot with labeled contour lines - Bottom (full width): Two cross-section line plots: - z vs x at y=0 (blue) - z vs x at y=π/2 (red)

Requirements: 1. Consistent colormap across surface, heatmap, and contour 2. Shared colorbar for the three 2D representations 3. Proper axis labels with LaTeX formatting 4. Super title and panel labels (A, B, C, D) 5. Custom GridSpec with width_ratios and height_ratios 6. Tight layout with no overlapping elements


Part 4: Expert Techniques

Q35. How do I create custom tick formatters?

Answer: Use FuncFormatter from matplotlib.ticker.

from matplotlib.ticker import FuncFormatter, MultipleLocator

fig, axs = plt.subplots(1, 3, figsize=(14, 4))

# Percentage formatter
def percent_fmt(x, pos):
    return f'{x*100:.0f}%'

x = np.linspace(0, 1, 50)
y = x**2
axs[0].plot(x, y, linewidth=2)
axs[0].xaxis.set_major_formatter(FuncFormatter(percent_fmt))
axs[0].yaxis.set_major_formatter(FuncFormatter(percent_fmt))
axs[0].set_title("Percentage Formatter")
axs[0].grid(alpha=0.3)

# Currency formatter
def currency_fmt(x, pos):
    if x >= 1e6:
        return f'${x/1e6:.1f}M'
    elif x >= 1e3:
        return f'${x/1e3:.0f}K'
    return f'${x:.0f}'

sales = np.array([125000, 340000, 890000, 1200000, 2500000])
axs[1].bar(range(5), sales, color='#2a9d8f')
axs[1].yaxis.set_major_formatter(FuncFormatter(currency_fmt))
axs[1].set_xticklabels(['Q1', 'Q2', 'Q3', 'Q4', 'Q5'])
axs[1].set_xticks(range(5))
axs[1].set_title("Currency Formatter")

# Scientific formatter with custom precision
def sci_fmt(x, pos):
    if x == 0:
        return '0'
    exp = int(np.floor(np.log10(abs(x))))
    coef = x / 10**exp
    return f'{coef:.1f}×10$^{{{exp}}}$'

x = np.linspace(0.001, 1, 100)
y = np.exp(x*10)
axs[2].semilogy(x, y, linewidth=2)
axs[2].set_title("Log Scale with Custom Labels")
axs[2].grid(alpha=0.3, which='both')

plt.tight_layout()
plt.show()

Q36. How do I create inset axes (zoom views)?

Answer: Use inset_axes from mpl_toolkits.axes_grid1.

from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset

fig, ax = plt.subplots(figsize=(10, 6))

# Main plot
x = np.linspace(0, 10, 1000)
y = np.sin(x**2 / 5)
ax.plot(x, y, linewidth=1.5, color='#264653')
ax.set_title("Main Plot with Inset Zoom")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.grid(alpha=0.3)

# Create inset axes
axins = inset_axes(ax, width="40%", height="40%", loc='upper right',
                   bbox_to_anchor=(0, 0, 0.95, 0.95), bbox_transform=ax.transAxes)

# Plot same data in inset
axins.plot(x, y, linewidth=1.5, color='#264653')
axins.set_xlim(2, 4)  # Zoom region
axins.set_ylim(-0.5, 1)
axins.grid(alpha=0.3)
axins.set_title("Zoom: x ∈ [2, 4]", fontsize=9)

# Mark the zoom region on main plot
ax.axvspan(2, 4, alpha=0.2, color='yellow')

plt.tight_layout()
plt.show()

Q37. How do I create custom legends?

Answer: Use custom handles, labels, and legend positioning options.

from matplotlib.lines import Line2D
from matplotlib.patches import Patch

fig, ax = plt.subplots(figsize=(10, 6))

# Plot multiple datasets
x = np.linspace(0, 10, 50)
ax.plot(x, np.sin(x), 'b-', linewidth=2, label='sin(x)')
ax.plot(x, np.cos(x), 'r--', linewidth=2, label='cos(x)')
ax.scatter(x[::5], np.sin(x[::5]), c='green', s=50)
ax.fill_between([4, 6], -1, 1, alpha=0.2, color='purple')

# Custom legend handles
legend_elements = [
    Line2D([0], [0], color='b', linewidth=2, label='sin(x)'),
    Line2D([0], [0], color='r', linewidth=2, linestyle='--', label='cos(x)'),
    Line2D([0], [0], marker='o', color='w', markerfacecolor='green', 
           markersize=10, label='Sample points'),
    Patch(facecolor='purple', alpha=0.2, label='Highlighted region')
]

ax.legend(handles=legend_elements, loc='upper center', ncol=2, 
          framealpha=0.9, fontsize=10, title="Legend", title_fontsize=11)

ax.set_title("Custom Legend with Different Handle Types")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

Q38. How do I create step plots and stem plots?

Answer: Use step() for stair-like data and stem() for discrete sequences.

fig, axs = plt.subplots(2, 2, figsize=(12, 9))

x = np.arange(10)
y = np.random.rand(10)

# Step plot - pre (left-continuous)
axs[0, 0].step(x, y, where='pre', linewidth=2, color='#264653', label="where='pre'")
axs[0, 0].plot(x, y, 'o', color='#e76f51', markersize=8)
axs[0, 0].set_title("Step Plot: pre (left-continuous)")
axs[0, 0].legend()
axs[0, 0].grid(alpha=0.3)

# Step plot - post (right-continuous)
axs[0, 1].step(x, y, where='post', linewidth=2, color='#2a9d8f', label="where='post'")
axs[0, 1].plot(x, y, 'o', color='#e76f51', markersize=8)
axs[0, 1].set_title("Step Plot: post (right-continuous)")
axs[0, 1].legend()
axs[0, 1].grid(alpha=0.3)

# Step plot - mid
axs[1, 0].step(x, y, where='mid', linewidth=2, color='#f4a261', label="where='mid'")
axs[1, 0].plot(x, y, 'o', color='#264653', markersize=8)
axs[1, 0].set_title("Step Plot: mid (centered)")
axs[1, 0].legend()
axs[1, 0].grid(alpha=0.3)

# Stem plot
markerline, stemlines, baseline = axs[1, 1].stem(x, y)
plt.setp(markerline, color='#e76f51', markersize=10)
plt.setp(stemlines, color='#264653', linewidth=1.5)
plt.setp(baseline, color='gray', linewidth=1)
axs[1, 1].set_title("Stem Plot: Discrete Sequence")
axs[1, 1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

Q39. How do I handle dates on axes?

Answer: Use matplotlib.dates for date formatting and locators.

import matplotlib.dates as mdates
from datetime import datetime, timedelta

# Generate date data
start_date = datetime(2025, 1, 1)
dates = [start_date + timedelta(days=i) for i in range(365)]
values = 100 + np.cumsum(np.random.randn(365) * 2)

fig, axs = plt.subplots(2, 1, figsize=(12, 8))

# Monthly locator
axs[0].plot(dates, values, linewidth=1.5, color='#264653')
axs[0].xaxis.set_major_locator(mdates.MonthLocator())
axs[0].xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
axs[0].set_title("Date Formatting: Monthly Ticks")
axs[0].grid(alpha=0.3)
plt.setp(axs[0].xaxis.get_majorticklabels(), rotation=45, ha='right')

# Quarterly with custom format
axs[1].plot(dates, values, linewidth=1.5, color='#2a9d8f')
axs[1].xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 4, 7, 10]))
axs[1].xaxis.set_major_formatter(mdates.DateFormatter('%b\n%Y'))
axs[1].xaxis.set_minor_locator(mdates.MonthLocator())
axs[1].set_title("Date Formatting: Quarterly Major, Monthly Minor")
axs[1].grid(alpha=0.3, which='both')

plt.tight_layout()
plt.show()

Q40. How do I create fill between curves?

Answer: Use fill_between() with conditions for selective filling.

fig, axs = plt.subplots(1, 3, figsize=(14, 4))
x = np.linspace(0, 10, 200)
y1 = np.sin(x)
y2 = np.sin(x) * 0.5

# Basic fill between
axs[0].plot(x, y1, 'b-', linewidth=2, label='y1')
axs[0].plot(x, y2, 'r-', linewidth=2, label='y2')
axs[0].fill_between(x, y1, y2, alpha=0.3, color='purple')
axs[0].set_title("Basic Fill Between")
axs[0].legend()
axs[0].grid(alpha=0.3)

# Conditional fill (where y1 > y2)
axs[1].plot(x, y1, 'b-', linewidth=2, label='y1')
axs[1].plot(x, y2, 'r-', linewidth=2, label='y2')
axs[1].fill_between(x, y1, y2, where=(y1 > y2), alpha=0.5, color='green', label='y1 > y2')
axs[1].fill_between(x, y1, y2, where=(y1 <= y2), alpha=0.5, color='red', label='y1 ≤ y2')
axs[1].set_title("Conditional Fill")
axs[1].legend()
axs[1].grid(alpha=0.3)

# Confidence bands
mean = np.sin(x) * np.exp(-x/10)
std = 0.2
axs[2].plot(x, mean, 'k-', linewidth=2, label='Mean')
axs[2].fill_between(x, mean - std, mean + std, alpha=0.3, color='blue', label='±1σ')
axs[2].fill_between(x, mean - 2*std, mean + 2*std, alpha=0.15, color='blue', label='±2σ')
axs[2].set_title("Confidence Bands")
axs[2].legend()
axs[2].grid(alpha=0.3)

plt.tight_layout()
plt.show()

Q41. How do I create annotated heatmaps efficiently?

Answer: Use a reusable function with proper text color contrast.

def annotated_heatmap(data, row_labels, col_labels, ax=None, cmap='viridis', 
                      fmt='.2f', cbar_label='', title=''):
    """Create an annotated heatmap with automatic text color selection."""
    if ax is None:
        ax = plt.gca()
    
    im = ax.imshow(data, cmap=cmap)
    
    # Color bar
    cbar = ax.figure.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
    cbar.ax.set_ylabel(cbar_label, rotation=-90, va="bottom")
    
    # Ticks
    ax.set_xticks(np.arange(len(col_labels)))
    ax.set_yticks(np.arange(len(row_labels)))
    ax.set_xticklabels(col_labels)
    ax.set_yticklabels(row_labels)
    
    # Rotate tick labels
    plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")
    
    # Annotate cells with automatic text color
    text_threshold = (data.max() + data.min()) / 2
    for i in range(len(row_labels)):
        for j in range(len(col_labels)):
            color = "white" if data[i, j] > text_threshold else "black"
            ax.text(j, i, format(data[i, j], fmt), ha="center", va="center", 
                   color=color, fontsize=9)
    
    ax.set_title(title)
    return im

# Demo
np.random.seed(42)
data = np.random.rand(6, 8) * 100
rows = [f'Row {i+1}' for i in range(6)]
cols = [f'Col {i+1}' for i in range(8)]

fig, ax = plt.subplots(figsize=(10, 6))
annotated_heatmap(data, rows, cols, ax=ax, cmap='YlOrRd', fmt='.1f', 
                  cbar_label='Value', title='Reusable Annotated Heatmap')
plt.tight_layout()
plt.show()

Q42. How do I save plots in publication-quality formats?

Answer: Use high DPI for raster, and vector formats (PDF/SVG) for publications.

fig, ax = plt.subplots(figsize=(8, 5))
x = np.linspace(0, 2*np.pi, 100)
ax.plot(x, np.sin(x), linewidth=2, label=r'$\sin(x)$')
ax.plot(x, np.cos(x), linewidth=2, linestyle='--', label=r'$\cos(x)$')
ax.set_xlabel(r'$x$ (radians)', fontsize=12)
ax.set_ylabel(r'$f(x)$', fontsize=12)
ax.set_title('Publication-Ready Figure', fontsize=14)
ax.legend(fontsize=11)
ax.grid(alpha=0.3)

# Different save options (uncomment to save)
# fig.savefig('figure.png', dpi=300, bbox_inches='tight', facecolor='white')  # Web/slides
# fig.savefig('figure.pdf', bbox_inches='tight')  # LaTeX publications
# fig.savefig('figure.svg', bbox_inches='tight')  # Scalable, editable
# fig.savefig('figure.eps', bbox_inches='tight')  # Some journals require EPS

print("Save options:")
print("- PNG: dpi=300+ for print, dpi=150 for web")
print("- PDF: Vector, perfect for LaTeX")  
print("- SVG: Editable in Inkscape/Illustrator")
print("- EPS: Legacy format for some journals")

plt.tight_layout()
plt.show()
Save options:
- PNG: dpi=300+ for print, dpi=150 for web
- PDF: Vector, perfect for LaTeX
- SVG: Editable in Inkscape/Illustrator
- EPS: Legacy format for some journals

Q43. How do I create reusable plotting functions?

Answer: Define functions that accept data and styling parameters, returning figure/axes.

def create_comparison_plot(data_dict, title='', xlabel='', ylabel='', 
                           figsize=(10, 6), style_config=None):
    """
    Create a comparison line plot with multiple datasets.
    
    Parameters:
    -----------
    data_dict : dict
        Dictionary with labels as keys and (x, y) tuples as values
    style_config : dict, optional
        Custom styling configuration
    
    Returns:
    --------
    fig, ax : matplotlib figure and axes objects
    """
    default_style = {
        'linewidth': 2,
        'grid_alpha': 0.3,
        'title_fontsize': 14,
        'label_fontsize': 11,
        'legend_fontsize': 10
    }
    style = {**default_style, **(style_config or {})}
    
    fig, ax = plt.subplots(figsize=figsize)
    
    for i, (label, (x, y)) in enumerate(data_dict.items()):
        ax.plot(x, y, linewidth=style['linewidth'], label=label, color=f'C{i}')
    
    ax.set_title(title, fontsize=style['title_fontsize'])
    ax.set_xlabel(xlabel, fontsize=style['label_fontsize'])
    ax.set_ylabel(ylabel, fontsize=style['label_fontsize'])
    ax.legend(fontsize=style['legend_fontsize'])
    ax.grid(alpha=style['grid_alpha'])
    
    return fig, ax

# Usage example
x = np.linspace(0, 10, 100)
data = {
    'Linear': (x, x),
    'Quadratic': (x, x**2 / 10),
    'Logarithmic': (x, np.log(x + 1) * 3),
    'Square Root': (x, np.sqrt(x) * 2)
}

fig, ax = create_comparison_plot(
    data, 
    title='Growth Functions Comparison',
    xlabel='Input (x)', 
    ylabel='Output f(x)'
)
plt.tight_layout()
plt.show()

Q44. How do I use rcParams for global styling?

Answer: Modify plt.rcParams or use rc_context for temporary changes.

# Show current defaults
print("Default font size:", plt.rcParams['font.size'])
print("Default figure size:", plt.rcParams['figure.figsize'])

# Create custom rcParams dictionary
custom_params = {
    'figure.figsize': (10, 6),
    'font.size': 12,
    'font.family': 'sans-serif',
    'axes.titlesize': 14,
    'axes.labelsize': 12,
    'xtick.labelsize': 10,
    'ytick.labelsize': 10,
    'legend.fontsize': 10,
    'axes.grid': True,
    'grid.alpha': 0.3,
    'lines.linewidth': 2,
    'axes.spines.top': False,
    'axes.spines.right': False,
}

# Use context manager for temporary style
with plt.rc_context(custom_params):
    fig, ax = plt.subplots()
    x = np.linspace(0, 10, 100)
    ax.plot(x, np.sin(x), label='sin(x)')
    ax.plot(x, np.cos(x), label='cos(x)')
    ax.set_title('Plot with Custom rcParams')
    ax.set_xlabel('X axis')
    ax.set_ylabel('Y axis')
    ax.legend()

plt.show()
Default font size: 10.0
Default figure size: [7.0, 5.0]

Q45. How do I create quiver plots for vector fields?

Answer: Use quiver() to visualize vector fields with arrows.

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

# Simple vector field
x = np.linspace(-2, 2, 10)
y = np.linspace(-2, 2, 10)
X, Y = np.meshgrid(x, y)
U = -Y  # Vector x-component
V = X   # Vector y-component

axs[0].quiver(X, Y, U, V, color='#264653')
axs[0].set_title("Rotation Vector Field")
axs[0].set_xlabel("x")
axs[0].set_ylabel("y")
axs[0].set_aspect('equal')
axs[0].grid(alpha=0.3)

# Colored by magnitude
x = np.linspace(-2, 2, 15)
y = np.linspace(-2, 2, 15)
X, Y = np.meshgrid(x, y)
U = X * np.exp(-X**2 - Y**2)
V = Y * np.exp(-X**2 - Y**2)
magnitude = np.sqrt(U**2 + V**2)

quiv = axs[1].quiver(X, Y, U, V, magnitude, cmap='viridis', scale=3)
plt.colorbar(quiv, ax=axs[1], label='Magnitude')
axs[1].set_title("Vector Field Colored by Magnitude")
axs[1].set_xlabel("x")
axs[1].set_ylabel("y")
axs[1].set_aspect('equal')

plt.tight_layout()
plt.show()

Q46. How do I create streamplots for flow visualization?

Answer: Use streamplot() for continuous flow lines.

fig, axs = plt.subplots(1, 2, figsize=(13, 5))

x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)

# Saddle point flow
U = X
V = -Y
axs[0].streamplot(X, Y, U, V, color='#264653', density=1.5, linewidth=1)
axs[0].set_title("Streamplot: Saddle Point")
axs[0].set_xlabel("x")
axs[0].set_ylabel("y")
axs[0].set_aspect('equal')

# Vortex with color by speed
U = -Y / (X**2 + Y**2 + 0.1)
V = X / (X**2 + Y**2 + 0.1)
speed = np.sqrt(U**2 + V**2)
speed[speed > 5] = 5  # Clip for better visualization

strm = axs[1].streamplot(X, Y, U, V, color=speed, cmap='plasma', density=1.5, 
                          linewidth=1, arrowsize=1)
plt.colorbar(strm.lines, ax=axs[1], label='Speed')
axs[1].set_title("Streamplot: Vortex (colored by speed)")
axs[1].set_xlabel("x")
axs[1].set_ylabel("y")
axs[1].set_aspect('equal')
axs[1].set_xlim(-3, 3)
axs[1].set_ylim(-3, 3)

plt.tight_layout()
plt.show()

Q47. How do I add images to plots?

Answer: Use imshow() for displaying images or imread() to load external images.

# Create synthetic images
fig, axs = plt.subplots(1, 3, figsize=(14, 4))

# Grayscale image
np.random.seed(42)
gray_img = np.random.rand(100, 100)
axs[0].imshow(gray_img, cmap='gray')
axs[0].set_title("Grayscale Image")
axs[0].axis('off')

# RGB image (synthetic gradient)
r = np.linspace(0, 1, 100).reshape(1, -1) * np.ones((100, 1))
g = np.linspace(0, 1, 100).reshape(-1, 1) * np.ones((1, 100))
b = 1 - (r + g) / 2
rgb_img = np.stack([r, g, b], axis=2)
axs[1].imshow(rgb_img)
axs[1].set_title("Synthetic RGB Gradient")
axs[1].axis('off')

# Image with custom extent and interpolation
pattern = np.sin(np.linspace(0, 4*np.pi, 100).reshape(1, -1)) * \
          np.cos(np.linspace(0, 4*np.pi, 100).reshape(-1, 1))
axs[2].imshow(pattern, cmap='RdBu_r', extent=[-5, 5, -5, 5], interpolation='bilinear')
axs[2].set_title("Pattern with Custom Extent")
axs[2].set_xlabel("X")
axs[2].set_ylabel("Y")

plt.tight_layout()
plt.show()

Q48. How do I create hexbin plots for large scatter data?

Answer: Use hexbin() for efficiently visualizing large point datasets.

np.random.seed(42)
n = 10000
x = np.random.randn(n)
y = x + np.random.randn(n) * 0.5

fig, axs = plt.subplots(1, 3, figsize=(14, 4))

# Regular scatter (slow and overlapping)
axs[0].scatter(x, y, alpha=0.1, s=5)
axs[0].set_title(f"Scatter: n={n} points")
axs[0].set_xlabel("X")
axs[0].set_ylabel("Y")

# Hexbin plot
hb = axs[1].hexbin(x, y, gridsize=30, cmap='YlOrRd', mincnt=1)
plt.colorbar(hb, ax=axs[1], label='Count')
axs[1].set_title("Hexbin: Efficient Density")
axs[1].set_xlabel("X")
axs[1].set_ylabel("Y")

# Hexbin with log scale
hb2 = axs[2].hexbin(x, y, gridsize=30, cmap='viridis', mincnt=1, bins='log')
plt.colorbar(hb2, ax=axs[2], label='log10(Count)')
axs[2].set_title("Hexbin: Log Scale")
axs[2].set_xlabel("X")
axs[2].set_ylabel("Y")

plt.tight_layout()
plt.show()

Q49. How do I create animations?

Answer: Use FuncAnimation from matplotlib.animation.

from matplotlib.animation import FuncAnimation
from IPython.display import HTML

# Create figure
fig, ax = plt.subplots(figsize=(8, 5))
ax.set_xlim(0, 2*np.pi)
ax.set_ylim(-1.5, 1.5)
ax.set_title("Animated Sine Wave")
ax.set_xlabel("x")
ax.set_ylabel("sin(x + φ)")
ax.grid(alpha=0.3)

line, = ax.plot([], [], 'b-', linewidth=2)
point, = ax.plot([], [], 'ro', markersize=10)

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

def init():
    line.set_data([], [])
    point.set_data([], [])
    return line, point

def animate(frame):
    phase = frame * 0.1
    y = np.sin(x + phase)
    line.set_data(x, y)
    point.set_data([phase % (2*np.pi)], [np.sin(2*phase)])
    return line, point

# Create animation (showing just the setup, actual animation requires display)
# anim = FuncAnimation(fig, animate, init_func=init, frames=100, interval=50, blit=True)

# To save: anim.save('animation.gif', writer='pillow', fps=20)
# To display in notebook: HTML(anim.to_jshtml())

print("Animation setup complete. Use FuncAnimation for live animations.")
print("Save with: anim.save('animation.gif', writer='pillow')")
plt.show()
Animation setup complete. Use FuncAnimation for live animations.
Save with: anim.save('animation.gif', writer='pillow')

Q50. How do I create publication-ready multi-panel figures?

Answer: Combine all techniques: GridSpec, consistent styling, proper spacing, and labels.

# Set publication style
plt.rcParams.update({
    'font.size': 10,
    'axes.labelsize': 11,
    'axes.titlesize': 11,
    'legend.fontsize': 9,
    'axes.spines.top': False,
    'axes.spines.right': False,
})

fig = plt.figure(figsize=(12, 10))
gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.35, wspace=0.35)

np.random.seed(42)
x = np.linspace(0, 10, 100)

# Panel A: Main time series
ax_a = fig.add_subplot(gs[0, :2])
ax_a.plot(x, np.sin(x) + np.random.randn(100)*0.1, linewidth=1.5, color='#264653')
ax_a.fill_between(x, np.sin(x)-0.2, np.sin(x)+0.2, alpha=0.2, color='#264653')
ax_a.set_title("A) Time Series with Confidence Interval")
ax_a.set_xlabel("Time (s)")
ax_a.set_ylabel("Amplitude")
ax_a.grid(alpha=0.3)

# Panel B: Distribution
ax_b = fig.add_subplot(gs[0, 2])
data = np.random.randn(500)
ax_b.hist(data, bins=25, color='#2a9d8f', edgecolor='white', alpha=0.8, density=True)
xd = np.linspace(-4, 4, 100)
ax_b.plot(xd, 1/np.sqrt(2*np.pi)*np.exp(-xd**2/2), 'r-', linewidth=2, label='Theory')
ax_b.set_title("B) Distribution")
ax_b.set_xlabel("Value")
ax_b.set_ylabel("Density")
ax_b.legend()

# Panel C: Scatter correlation
ax_c = fig.add_subplot(gs[1, 0])
x_scatter = np.random.randn(100)
y_scatter = 0.7*x_scatter + np.random.randn(100)*0.5
ax_c.scatter(x_scatter, y_scatter, alpha=0.6, c='#e76f51', edgecolors='white', linewidth=0.5)
ax_c.set_title("C) Correlation (r=0.81)")
ax_c.set_xlabel("Variable X")
ax_c.set_ylabel("Variable Y")
ax_c.grid(alpha=0.3)

# Panel D: Bar comparison
ax_d = fig.add_subplot(gs[1, 1])
categories = ['Ctrl', 'Treat1', 'Treat2', 'Treat3']
values = [10, 15, 18, 12]
errors = [1, 2, 1.5, 1.8]
colors = ['#264653', '#2a9d8f', '#e9c46a', '#e76f51']
ax_d.bar(categories, values, yerr=errors, capsize=4, color=colors, edgecolor='white')
ax_d.set_title("D) Treatment Comparison")
ax_d.set_ylabel("Response")
ax_d.grid(axis='y', alpha=0.3)

# Panel E: Box plot
ax_e = fig.add_subplot(gs[1, 2])
data_box = [np.random.randn(50) + i for i in range(4)]
bp = ax_e.boxplot(data_box, labels=['G1', 'G2', 'G3', 'G4'], patch_artist=True)
for patch, color in zip(bp['boxes'], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.7)
ax_e.set_title("E) Group Distribution")
ax_e.set_ylabel("Value")
ax_e.grid(axis='y', alpha=0.3)

# Panel F: Heatmap (spanning bottom row)
ax_f = fig.add_subplot(gs[2, :])
heatmap_data = np.random.rand(5, 10) * 100
im = ax_f.imshow(heatmap_data, cmap='YlOrRd', aspect='auto')
ax_f.set_xticks(range(10))
ax_f.set_yticks(range(5))
ax_f.set_xticklabels([f'S{i+1}' for i in range(10)])
ax_f.set_yticklabels([f'Gene {i+1}' for i in range(5)])
ax_f.set_title("F) Expression Heatmap")
plt.colorbar(im, ax=ax_f, shrink=0.6, label='Expression Level')

plt.suptitle("Multi-Panel Research Figure", fontsize=14, fontweight='bold', y=1.02)
plt.tight_layout()
plt.show()

# Reset rcParams
plt.rcParams.update(plt.rcParamsDefault)

Challenge 1 (Easy): Custom Tick Formatter

Create a bar chart of values in thousands (e.g., [15000, 28000, 42000, 38000, 51000]).

Requirements: - Y-axis shows formatted values like “15K”, “28K”, etc. - Use FuncFormatter from matplotlib.ticker - Title and proper labels


Challenge 2 (Easy): Basic Animation Setup

Create a static frame showing the setup for an animated sine wave.

Requirements: - Draw a sine wave that would animate through phase shifts - Add a moving marker point on the curve - Set axis limits appropriately - Add title showing frame number placeholder


Challenge 3 (Moderate): Inset Zoom with Connection

Plot a noisy signal over 100 points and add an inset zoom view.

Requirements: - Main plot: full signal with noise - Inset axes (30% width, 30% height) in upper-right - Zoom into the region x ∈ [40, 60] - Visual connector lines/box highlighting the zoomed region - Different background color for inset


Challenge 4 (Moderate): Custom rcParams Theme

Create a reusable plotting theme and apply it to multiple plots.

Requirements: - Define a custom style dictionary with: - Sans-serif font family - Larger font sizes (12pt base) - No top/right spines - Custom color cycle (at least 5 colors) - Grid enabled by default - Create a 1×3 subplot showing line, bar, and scatter plots - All plots should inherit the custom theme


Challenge 5 (Difficult): Complete Publication Figure with Everything

Create a journal-ready figure combining multiple advanced techniques:

Layout: 2×3 grid using GridSpec with varying sizes

Panel A (top-left, large): Time series with: - Main line and confidence band (fill_between) - Inset zoom of peak region - Custom date formatting on x-axis

Panel B (top-right): Polar radar chart showing 6 metrics

Panel C (middle-left): Violin plot with overlaid strip plot (jittered points)

Panel D (middle-right): Annotated heatmap with custom colormap

Panel E (bottom, full width): Streamplot of a vector field with: - Colored by magnitude - Quiver overlay at sparse points

Requirements: 1. Consistent color palette throughout 2. Panel labels (A, B, C, D, E) in corners 3. LaTeX-formatted axis labels where appropriate 4. All fonts suitable for publication (11pt minimum) 5. No overlapping elements with tight_layout 6. Export-ready at 300 DPI


Quick Reference Guide

Common Plot Types

Function Use Case
plot() Line charts, time series
scatter() Relationships, clusters
bar() / barh() Categorical comparisons
hist() Distributions
pie() Part of whole (sparingly)
boxplot() Statistical summaries
violinplot() Distribution shape
errorbar() Data with uncertainty
fill_between() Confidence bands
imshow() / pcolormesh() Heatmaps, images
contour() / contourf() 2D scalar fields
quiver() Vector fields
streamplot() Flow visualization
hexbin() Large point datasets

Styling Cheat Sheet

# Figure
fig, ax = plt.subplots(figsize=(width, height), dpi=100)

# Lines
ax.plot(x, y, color='red', linestyle='--', linewidth=2, marker='o', markersize=8, alpha=0.8, label='Data')

# Text
ax.set_title('Title', fontsize=14, fontweight='bold')
ax.set_xlabel('X Label', fontsize=12)
ax.text(x, y, 'annotation', fontsize=10, ha='center', va='center')

# Legend
ax.legend(loc='upper right', framealpha=0.9, ncol=2)

# Grid and spines
ax.grid(alpha=0.3, linestyle='--')
ax.spines['top'].set_visible(False)

# Save
fig.savefig('plot.png', dpi=300, bbox_inches='tight', facecolor='white')

Summary

This comprehensive guide covered:

  • 50+ questions from fundamentals to expert techniques
  • Basic plots: line, bar, scatter, histogram, pie
  • Statistical visualization: box plots, violin plots, error bars
  • Advanced techniques: 3D plots, heatmaps, contours, polar plots
  • Professional skills: custom formatters, animations, publication figures
  • Best practices: reusable code, style management, export options
Tip

Key takeaway: Master the object-oriented API for maximum flexibility, and always choose the chart type that best tells your data story.


References

  1. Matplotlib Official Documentation
  2. Matplotlib Gallery
  3. Matplotlib Tutorials
  4. Scientific Visualization: Python + Matplotlib (Book)
  5. Matplotlib Cheatsheets