Note
Go to the end to download the full example code.
Publication-Ready Multi-Panel Figure¶
A 2×2 scientific figure combining four fundamental plot types — exactly
as they would appear in an academic paper. Each panel uses the scientific
preset and is annotated with dm.label_axes() for automatic (a)–(d)
panel indexing. Typography is unified via dm.fs() and dm.lw()
scaling helpers.

import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import numpy as np
import dartwork_mpl as dm
dm.style.use("scientific")
np.random.seed(42)
fig = plt.figure(figsize=(dm.DW, dm.DW * 0.90))
gs = gridspec.GridSpec(2, 2, figure=fig, hspace=0.55, wspace=0.45)
# ── (a) Line plot with error band ──
ax = fig.add_subplot(gs[0, 0])
x = np.linspace(0, 4 * np.pi, 150)
y_true = np.sin(x) * np.exp(-0.15 * x)
y_noise = y_true + np.random.normal(0, 0.08, len(x))
ax.plot(x, y_true, color="oc.blue7", lw=dm.lw(1), label="Model")
band = dm.pseudo_alpha("oc.blue5", 0.15, background="white")
ax.fill_between(x, y_true - 0.15, y_true + 0.15, color=band, label="95% CI")
ax.scatter(
x[::8], y_noise[::8], s=8, color="oc.gray6", zorder=3, label="Observations"
)
ax.set_title("Damped Oscillation", fontsize=dm.fs(0), weight="bold", pad=12)
ax.set_xlabel("Time (s)")
ax.set_ylabel("Amplitude")
ax.set_ylim(-1.2, 1.8)
ax.legend(fontsize=dm.fs(-1), loc="upper right", frameon=False)
# ── (b) Scatter with regression ──
ax = fig.add_subplot(gs[0, 1])
n_pts = 80
x_s = np.random.uniform(0, 10, n_pts)
y_s = 1.8 * x_s + 3 + np.random.normal(0, 3, n_pts)
ax.scatter(x_s, y_s, s=18, color="oc.grape5", alpha=0.7, edgecolors="none")
m, b = np.polyfit(x_s, y_s, 1)
x_fit = np.linspace(0, 10, 50)
ax.plot(
x_fit,
m * x_fit + b,
color="oc.red7",
lw=dm.lw(1),
label=f"y = {m:.1f}x + {b:.1f}",
)
ax.set_title("Linear Regression", fontsize=dm.fs(0), weight="bold", pad=12)
ax.set_xlabel("Feature X")
ax.set_ylabel("Response Y")
ax.set_ylim(0, 35)
ax.legend(fontsize=dm.fs(-1), loc="upper left", frameon=False)
# ── (c) Histogram ──
ax = fig.add_subplot(gs[1, 0])
data1 = np.random.normal(50, 8, 500)
data2 = np.random.normal(65, 10, 500)
ax.hist(
data1,
bins=25,
color=dm.pseudo_alpha("tw.teal500", 0.6, background="white"),
edgecolor="white",
lw=0.5,
label="Group A",
)
ax.hist(
data2,
bins=25,
color=dm.pseudo_alpha("tw.rose500", 0.5, background="white"),
edgecolor="white",
lw=0.5,
label="Group B",
)
ax.set_title(
"Distribution Comparison", fontsize=dm.fs(0), weight="bold", pad=12
)
ax.set_xlabel("Value")
ax.set_ylabel("Frequency")
ax.set_ylim(0, 110)
ax.legend(fontsize=dm.fs(-1), frameon=False)
# ── (d) Box plot ──
ax = fig.add_subplot(gs[1, 1])
groups = [np.random.normal(loc, 5, 60) for loc in [40, 55, 48, 62, 45]]
bp = ax.boxplot(
groups,
patch_artist=True,
widths=0.6,
medianprops={"color": "oc.red7", "lw": dm.lw(0)},
)
box_colors = ["oc.blue3", "oc.grape3", "oc.teal3", "oc.orange3", "oc.pink3"]
for patch, color in zip(bp["boxes"], box_colors, strict=False):
patch.set_facecolor(color)
patch.set_edgecolor("oc.gray6")
ax.set_xticklabels(["A", "B", "C", "D", "E"])
ax.set_title("Group Comparison", fontsize=dm.fs(0), weight="bold", pad=12)
ax.set_xlabel("Group")
ax.set_ylabel("Measurement")
# Panel labels & layout
dm.label_axes(fig.axes)
for a in fig.axes:
dm.set_decimal(a, yn=0)
dm.simple_layout(fig)
plt.show()
Total running time of the script: (0 minutes 3.990 seconds)