Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Low-level Python plotting for scientific figures: publication-quality line, scatter, bar, heatmap, contour, 3D; multi-panel layouts; fine control of every element. PNG/PDF/SVG export. Use seaborn for quick stats, plotly for interactive.
.claude/skills/jaechang-hits-matplotlib-scientific-plotting/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 198% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 136% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 231% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 147% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 92% | 0% |
Matplotlib is Python's foundational visualization library for creating static, animated, and interactive plots. It provides both a MATLAB-style pyplot interface and an object-oriented API for full control over figures, axes, and artists. Essential for generating publication-quality scientific figures.
seaborn insteadplotly insteadmatplotlib, numpypandas (for DataFrame plotting), seaborn (for style presets)%matplotlib inline), and GUI appsbashpip install matplotlib numpy
pythonimport matplotlib.pyplot as plt import numpy as np # Publication-ready figure template: set size, plot, label, save as PDF fig, ax = plt.subplots(figsize=(6, 4)) # single-column journal width ≈ 6 cm → set here in inches x = np.linspace(0, 2 * np.pi, 200) ax.plot(x, np.sin(x), color="steelblue", lw=1.5, label="sin(x)") ax.plot(x, np.cos(x), color="coral", lw=1.5, label="cos(x)", linestyle="--") ax.set_xlabel("x (radians)") ax.set_ylabel("Amplitude") ax.set_title("Sine and Cosine Waves") ax.legend(frameon=False) ax.spines[["top", "right"]].set_visible(False) # clean axis style plt.tight_layout() plt.savefig("quickstart.pdf", bbox_inches="tight", dpi=300) print("Saved quickstart.pdf")
The fundamental objects: Figure (canvas) and Axes (plotting area).
pythonimport matplotlib.pyplot as plt import numpy as np # Single plot (recommended: OO interface) fig, ax = plt.subplots(figsize=(8, 5)) x = np.linspace(0, 2 * np.pi, 100) ax.plot(x, np.sin(x), label="sin(x)") ax.plot(x, np.cos(x), label="cos(x)") ax.set_xlabel("x"); ax.set_ylabel("y") ax.set_title("Trigonometric Functions") ax.legend(); ax.grid(True, alpha=0.3) plt.savefig("basic_plot.png", dpi=300, bbox_inches="tight") print("Saved basic_plot.png")
python# Multi-panel subplots fig, axes = plt.subplots(2, 2, figsize=(10, 8), constrained_layout=True) axes[0, 0].plot(x, np.sin(x)); axes[0, 0].set_title("sin(x)") axes[0, 1].scatter(x[::5], np.cos(x[::5])); axes[0, 1].set_title("cos(x)") axes[1, 0].bar(["A", "B", "C"], [3, 7, 5]); axes[1, 0].set_title("Bar") axes[1, 1].hist(np.random.randn(500), bins=30); axes[1, 1].set_title("Histogram") plt.savefig("subplots.png", dpi=300, bbox_inches="tight") print("Saved subplots.png with 4 panels")
Standard scientific chart types.
pythonimport matplotlib.pyplot as plt import numpy as np fig, axes = plt.subplots(2, 3, figsize=(15, 9), constrained_layout=True) # Line plot — trends over time x = np.linspace(0, 10, 50) axes[0, 0].plot(x, np.exp(-x/3) * np.sin(x), "b-", linewidth=2) axes[0, 0].set_title("Line Plot") # Scatter plot — correlations np.random.seed(42) axes[0, 1].scatter(np.random.randn(100), np.random.randn(100), alpha=0.6, c=np.random.rand(100), cmap="viridis") axes[0, 1].set_title("Scatter Plot") # Bar chart — categorical comparisons categories = ["Gene A", "Gene B", "Gene C", "Gene D"] axes[0, 2].bar(categories, [4.2, 7.1, 3.5, 6.8], color="steelblue", edgecolor="black") axes[0, 2].set_title("Bar Chart") # Histogram — distributions axes[1, 0].hist(np.random.randn(1000), bins=40, edgecolor="black", alpha=0.7) axes[1, 0].set_title("Histogram") # Box plot — statistical distributions data = [np.random.randn(50) + i for i in range(4)] axes[1, 1].boxplot(data, labels=["Ctrl", "Drug A", "Drug B", "Drug C"]) axes[1, 1].set_title("Box Plot") # Heatmap — matrix data matrix = np.random.rand(8, 8) im = axes[1, 2].imshow(matrix, cmap="coolwarm", aspect="auto") plt.colorbar(im, ax=axes[1, 2]) axes[1, 2].set_title("Heatmap") plt.savefig("plot_types.png", dpi=300, bbox_inches="tight") print("Saved 6 plot types to plot_types.png")
Colors, fonts, styles, annotations.
pythonimport matplotlib.pyplot as plt import numpy as np # Use style sheets plt.style.use("seaborn-v0_8-whitegrid") # Custom rcParams for publication plt.rcParams.update({ "font.size": 12, "axes.labelsize": 14, "axes.titlesize": 16, "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.fontsize": 11, }) fig, ax = plt.subplots(figsize=(8, 5)) x = np.linspace(0, 5, 100) ax.plot(x, np.exp(-x), "r--", linewidth=2, label="Exponential decay") ax.fill_between(x, np.exp(-x) - 0.1, np.exp(-x) + 0.1, alpha=0.2, color="red") # Annotations ax.annotate("Half-life", xy=(0.693, 0.5), xytext=(2, 0.7), arrowprops=dict(arrowstyle="->", color="black"), fontsize=12, fontweight="bold") ax.set_xlabel("Time (s)"); ax.set_ylabel("Signal") ax.legend() plt.savefig("styled_plot.png", dpi=300, bbox_inches="tight") print("Saved styled_plot.png")
Mosaic layouts, GridSpec, insets.
pythonimport matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec import numpy as np # Mosaic layout — named axes fig, axes = plt.subplot_mosaic( [["main", "right"], ["main", "bottom_right"]], figsize=(10, 7), constrained_layout=True, gridspec_kw={"width_ratios": [2, 1]} ) x = np.linspace(0, 10, 200) axes["main"].plot(x, np.sin(x) * np.exp(-x/5), "b-", linewidth=2) axes["main"].set_title("Main Panel") axes["right"].hist(np.random.randn(300), bins=20, orientation="horizontal") axes["right"].set_title("Distribution") axes["bottom_right"].bar(["A", "B"], [3, 5]) axes["bottom_right"].set_title("Summary") plt.savefig("mosaic_layout.png", dpi=300, bbox_inches="tight") print("Saved mosaic_layout.png")
Surface, scatter, and wireframe plots.
pythonimport matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np fig = plt.figure(figsize=(10, 7)) ax = fig.add_subplot(111, projection="3d") # Surface plot u = np.linspace(0, 2 * np.pi, 50) v = np.linspace(0, np.pi, 50) X = np.outer(np.cos(u), np.sin(v)) Y = np.outer(np.sin(u), np.sin(v)) Z = np.outer(np.ones_like(u), np.cos(v)) ax.plot_surface(X, Y, Z, cmap="viridis", alpha=0.8) ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z") ax.set_title("3D Surface Plot") plt.savefig("surface_3d.png", dpi=300, bbox_inches="tight") print("Saved surface_3d.png")
Output to various formats with publication settings.
pythonimport matplotlib.pyplot as plt import numpy as np fig, ax = plt.subplots(figsize=(6, 4)) ax.plot([1, 2, 3], [1, 4, 9], "ko-") ax.set_title("Export Example") # High-res PNG for presentations fig.savefig("figure.png", dpi=300, bbox_inches="tight", facecolor="white") # Vector PDF for journal submission fig.savefig("figure.pdf", bbox_inches="tight") # SVG for web fig.savefig("figure.svg", bbox_inches="tight") # Transparent background fig.savefig("figure_transparent.png", dpi=300, bbox_inches="tight", transparent=True) plt.close(fig) # Free memory print("Exported to PNG, PDF, SVG, and transparent PNG")
Goal: Create a 4-panel figure combining different plot types for a paper.
pythonimport matplotlib.pyplot as plt import numpy as np np.random.seed(42) fig, axes = plt.subplots(2, 2, figsize=(10, 8), constrained_layout=True) # Panel A: Time series t = np.linspace(0, 24, 100) axes[0, 0].plot(t, 50 + 10 * np.sin(t * np.pi / 12), "b-", linewidth=2) axes[0, 0].set_xlabel("Time (h)"); axes[0, 0].set_ylabel("Expression") axes[0, 0].set_title("A", loc="left", fontweight="bold") # Panel B: Volcano plot fc = np.random.randn(500) pval = -np.log10(np.random.uniform(0.0001, 1, 500)) colors = ["red" if abs(f) > 1 and p > 2 else "grey" for f, p in zip(fc, pval)] axes[0, 1].scatter(fc, pval, c=colors, s=10, alpha=0.7) axes[0, 1].axhline(2, ls="--", color="black", alpha=0.5) axes[0, 1].set_xlabel("log₂ FC"); axes[0, 1].set_ylabel("-log₁₀ p-value") axes[0, 1].set_title("B", loc="left", fontweight="bold") # Panel C: Bar chart with error bars means = [3.2, 5.1, 4.7, 6.3] sems = [0.4, 0.6, 0.3, 0.5] axes[1, 0].bar(["Ctrl", "Drug A", "Drug B", "Combo"], means, yerr=sems, capsize=5, color="steelblue", edgecolor="black") axes[1, 0].set_ylabel("Response"); axes[1, 0].set_title("C", loc="left", fontweight="bold") # Panel D: Heatmap data = np.random.randn(6, 4) im = axes[1, 1].imshow(data, cmap="RdBu_r", aspect="auto") plt.colorbar(im, ax=axes[1, 1]) axes[1, 1].set_title("D", loc="left", fontweight="bold") fig.savefig("publication_figure.pdf", bbox_inches="tight") print("Saved publication_figure.pdf (4 panels)")
Goal: Bar chart with individual data points and significance annotations.
pythonimport matplotlib.pyplot as plt import numpy as np np.random.seed(42) groups = {"Control": np.random.normal(5, 1.2, 20), "Treatment A": np.random.normal(7, 1.5, 20), "Treatment B": np.random.normal(6, 1.0, 20)} fig, ax = plt.subplots(figsize=(6, 5)) positions = range(len(groups)) for i, (name, data) in enumerate(groups.items()): ax.bar(i, np.mean(data), yerr=np.std(data)/np.sqrt(len(data)), capsize=5, color=["#4C72B0", "#DD8452", "#55A868"][i], edgecolor="black", alpha=0.8, width=0.6) # Overlay individual data points ax.scatter(np.full_like(data, i) + np.random.uniform(-0.15, 0.15, len(data)), data, color="black", s=15, alpha=0.5, zorder=5) ax.set_xticks(positions); ax.set_xticklabels(groups.keys()) ax.set_ylabel("Measurement") # Add significance bracket y_max = max(max(d) for d in groups.values()) + 1 ax.plot([0, 0, 1, 1], [y_max, y_max + 0.2, y_max + 0.2, y_max], "k-", linewidth=1) ax.text(0.5, y_max + 0.3, "**", ha="center", fontsize=14) fig.savefig("comparison_plot.png", dpi=300, bbox_inches="tight") print("Saved comparison_plot.png")
| Parameter | Module | Default | Range / Options | Effect | |-----------|--------|---------|-----------------|--------| | figsize | Figure creation | (6.4, 4.8) | (w, h) in inches | Figure dimensions | | dpi | savefig | 100 | 72-600 | Resolution: 300 for print, 150 for web | | bbox_inches | savefig | None | "tight", None | Crop whitespace around figure | | constrained_layout | subplots | False | True/False | Auto-adjust spacing to prevent overlap | | cmap | Heatmap/scatter | "viridis" | "viridis", "coolwarm", "RdBu_r", etc. | Colormap for data mapping | | alpha | All plot types | 1.0 | 0.0-1.0 | Transparency (0=invisible, 1=opaque) | | linewidth | Line plots | 1.5 | 0.5-5.0 | Line thickness in points | | s | Scatter | 20 | 1-500 | Marker size in points² | | bins | Histogram | 10 | 5-100 or array | Number of histogram bins | | projection | add_subplot | None | "3d", "polar" | Axes projection type |
fig, ax = plt.subplots()) for production code. Reserve plt.plot() for quick interactive exploration onlyconstrained_layout=True to prevent overlapping labels and titles:python fig, ax = plt.subplots(figsize=(8, 5), constrained_layout=True)
viridis, cividis, or plasma (perceptually uniform, colorblind-safe). Avoid jet and rainbowpython plt.close(fig) # After savefig
rasterized=True for large datasets to reduce PDF/SVG file size:python ax.scatter(x, y, rasterized=True) # Vector labels + rasterized data
When to use: Consistent colors across multiple figures in a paper.
pythonimport matplotlib.pyplot as plt # Define a custom palette palette = {"control": "#4C72B0", "treatment": "#DD8452", "combo": "#55A868"} fig, ax = plt.subplots() for group, color in palette.items(): ax.bar(group, [5, 7, 6][list(palette.keys()).index(group)], color=color) plt.savefig("custom_palette.png", dpi=300, bbox_inches="tight")
When to use: Plotting two variables with different scales on the same figure.
pythonimport matplotlib.pyplot as plt import numpy as np fig, ax1 = plt.subplots(figsize=(8, 5)) x = np.arange(10) ax1.bar(x, np.random.randint(10, 100, 10), alpha=0.7, color="steelblue", label="Count") ax1.set_ylabel("Count", color="steelblue") ax2 = ax1.twinx() ax2.plot(x, np.cumsum(np.random.rand(10)), "r-o", linewidth=2, label="Cumulative") ax2.set_ylabel("Cumulative", color="red") fig.legend(loc="upper left", bbox_to_anchor=(0.15, 0.95)) plt.savefig("twin_axes.png", dpi=300, bbox_inches="tight")
When to use: Showing a zoomed-in region of a larger plot.
pythonimport matplotlib.pyplot as plt import numpy as np fig, ax = plt.subplots(figsize=(8, 5)) x = np.linspace(0, 10, 500) y = np.sin(x) * np.exp(-x / 5) ax.plot(x, y, "b-", linewidth=2) # Inset axins = ax.inset_axes([0.5, 0.5, 0.4, 0.4]) axins.plot(x, y, "b-", linewidth=2) axins.set_xlim(1, 3); axins.set_ylim(0.2, 0.8) ax.indicate_inset_zoom(axins, edgecolor="black") plt.savefig("inset_zoom.png", dpi=300, bbox_inches="tight")
| Problem | Cause | Solution | |---------|-------|----------| | Overlapping labels/titles | No layout management | Add constrained_layout=True to plt.subplots() | | UserWarning: tight_layout | Incompatible with constrained_layout | Use only one: constrained_layout OR tight_layout(), not both | | Blurry figures in Jupyter | Low default DPI | Set %config InlineBackend.figure_format = 'retina' | | Memory grows in loops | Figures not closed | Add plt.close(fig) after each savefig() | | Font not found warning | Missing system font | Use plt.rcParams["font.sans-serif"] = ["DejaVu Sans"] | | Large PDF/SVG file size | Many data points in vector | Use rasterized=True on heavy artists | | 3D plot rotation stuck | Interactive backend issue | Use %matplotlib widget in Jupyter or plt.show() in scripts | | Colorbar wrong size | Default sizing doesn't match axes | Use fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04) |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 20,452 | 13,626 | -33% | 1 | 1 | 0% | 3,934 | 7,534 | +92% | 0 | 0 | — |
case-02 | pass→pass | 7,384 | 9,317 | +26% | 1 | 1 | 0% | 1,338 | 7,192 | +438% | 0 | 0 | — |
case-03 | fail→pass | 17,601 | 20,657 | +17% | 1 | 1 | 0% | 3,024 | 9,007 | +198% | 0 | 0 | — |
case-04 | fail→pass | 14,705 | 8,871 | -40% | 1 | 1 | 0% | 3,063 | 7,234 | +136% | 0 | 0 | — |
case-05 | pass→pass | 7,607 | 6,495 | -15% | 1 | 1 | 0% | 1,423 | 6,749 | +374% | 0 | 0 | — |
case-06 | pass→pass | 9,627 | 8,363 | -13% | 1 | 1 | 0% | 1,859 | 6,806 | +266% | 0 | 0 | — |
case-07 | pass→pass | 11,791 | 8,190 | -31% | 1 | 1 | 0% | 2,177 | 6,899 | +217% | 0 | 0 | — |
case-08 | pass→pass | 13,890 | 9,671 | -30% | 1 | 1 | 0% | 2,371 | 7,078 | +199% | 0 | 0 | — |
case-09 | pass→pass | 9,762 | 9,671 | -1% | 1 | 1 | 0% | 1,876 | 7,402 | +295% | 0 | 0 | — |
case-10 | pass→pass | 9,037 | 11,610 | +28% | 1 | 1 | 0% | 1,875 | 7,751 | +313% | 0 | 0 | — |
case-11 | pass→pass | 12,521 | 8,480 | -32% | 1 | 1 | 0% | 2,392 | 7,123 | +198% | 0 | 0 | — |
case-12 | pass→pass | 15,587 | 11,202 | -28% | 1 | 1 | 0% | 3,127 | 7,591 | +143% | 0 | 0 | — |
case-13 | fail→pass | 12,373 | 10,163 | -18% | 1 | 1 | 0% | 2,279 | 7,551 | +231% | 0 | 0 | — |
case-14 | pass→pass | 7,225 | 8,679 | +20% | 1 | 1 | 0% | 1,491 | 7,034 | +372% | 0 | 0 | — |
case-15 | pass→pass | 8,332 | 4,352 | -48% | 1 | 1 | 0% | 1,387 | 6,114 | +341% | 0 | 0 | — |
case-16 | fail→pass | 14,453 | 6,042 | -58% | 1 | 1 | 0% | 2,679 | 6,614 | +147% | 0 | 0 | — |
case-17 | pass→pass | 4,497 | 3,612 | -20% | 1 | 1 | 0% | 870 | 6,091 | +600% | 0 | 0 | — |
case-18 | pass→pass | 4,841 | 5,878 | +21% | 1 | 1 | 0% | 942 | 6,519 | +592% | 0 | 0 | — |
case-19 | pass→pass | 8,100 | 9,189 | +13% | 1 | 1 | 0% | 1,620 | 7,375 | +355% | 0 | 0 | — |
case-20 | pass→pass | 5,312 | 5,874 | +11% | 1 | 1 | 0% | 1,055 | 6,508 | +517% | 0 | 0 | — |
case-21 | pass→pass | 8,748 | 6,907 | -21% | 1 | 1 | 0% | 1,617 | 6,837 | +323% | 0 | 0 | — |
case-22 | pass→pass | 7,728 | 4,467 | -42% | 1 | 1 | 0% | 1,393 | 6,193 | +345% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.