Scientific Charts
Generate publication-quality charts for articles. All output is dark-themed, high-DPI PNG ready for Substack embedding. Never generate charts via LLM — always use this skill's Python scripts.
Prerequisites
pip install --break-system-packages matplotlib numpy # already done on this system
Chart Types
1. Comparison Bar Chart (Horizontal)
Use for: comparing treatments, compounds, protocols.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
labels = ['Placebo', 'SSRI', 'Ketamine IV', 'Psilocybin']
values = [30, 47, 71, 68]
colors = ['#64748b', '#64748b', '#6366f1', '#10b981']
fig, ax = plt.subplots(figsize=(10, 4))
fig.patch.set_facecolor('#1a1a2e')
ax.set_facecolor('#1a1a2e')
bars = ax.barh(labels, values, color=colors, height=0.6)
ax.set_xlabel('Response Rate (%)', color='#94a3b8', fontsize=11)
ax.set_title('Treatment Response Comparison', color='#e2e8f0', fontsize=14, fontweight='bold')
ax.tick_params(colors='#94a3b8', labelsize=10)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#334155')
ax.spines['bottom'].set_color('#334155')
for bar, val in zip(bars, values):
ax.text(val + 1, bar.get_y() + bar.get_height()/2, f'{val}%',
va='center', color='#e2e8f0', fontsize=10, fontweight='bold')
plt.tight_layout()
plt.savefig('chart.png', dpi=150, bbox_inches='tight', facecolor='#1a1a2e')
2. Dose-Response Curve
Use for: showing effect vs dose, receptor occupancy, concentration-response.
import numpy as np
import matplotlib.pyplot as plt
doses = np.logspace(-1, 2, 50)
response = 100 / (1 + np.exp(-(np.log10(doses) - 0.8) * 3))
fig, ax = plt.subplots(figsize=(8, 5))
fig.patch.set_facecolor('#1a1a2e')
ax.set_facecolor('#1a1a2e')
ax.plot(doses, response, color='#6366f1', linewidth=2.5)
ax.fill_between(doses, response - 8, response + 8, color='#6366f1', alpha=0.1)
ax.set_xscale('log')
ax.set_xlabel('Dose (mg/kg)', color='#94a3b8', fontsize=11)
ax.set_ylabel('Response (%)', color='#94a3b8', fontsize=11)
ax.set_title('Dose-Response Relationship', color='#e2e8f0', fontsize=14, fontweight='bold')
ax.tick_params(colors='#94a3b8')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#334155')
ax.spines['bottom'].set_color('#334155')
ax.grid(True, alpha=0.1, color='#94a3b8')
# Annotate key points
ax.annotate('ED50 ≈ 6.3 mg/kg', xy=(6.3, 50), xytext=(15, 35),
arrowprops=dict(arrowstyle='->', color='#f59e0b'),
color='#f59e0b', fontsize=9)
plt.tight_layout()
plt.savefig('dose_response.png', dpi=150, bbox_inches='tight', facecolor='#1a1a2e')
3. Timeline / Mechanism Sequence
Use for: showing order of molecular events after administration.
import matplotlib.pyplot as plt
events = [
('NMDAR blockade', 0, 5),
('Glutamate surge', 5, 30),
('AMPA activation', 10, 60),
('BDNF release', 30, 120),
('mTORC1 activation', 45, 180),
('Spine formation', 120, 480),
('Clinical effect', 110, 1440),
]
labels, starts, ends = zip(*events)
fig, ax = plt.subplots(figsize=(10, 4))
fig.patch.set_facecolor('#1a1a2e')
ax.set_facecolor('#1a1a2e')
colors = ['#6366f1', '#818cf8', '#a78bfa', '#c4b5fd', '#ec4899', '#f472b6', '#10b981']
for i, (label, start, end) in enumerate(events):
ax.barh(i, end - start, left=start, height=0.5, color=colors[i], edgecolor=None)
ax.text(end + 5, i, f'{start}-{end}min', va='center', color='#94a3b8', fontsize=8)
ax.set_yticks(range(len(events)))
ax.set_yticklabels(labels, color='#e2e8f0', fontsize=10)
ax.set_xlabel('Time after administration (minutes)', color='#94a3b8', fontsize=11)
ax.set_xscale('log')
ax.set_title('Molecular Cascade After Ketamine Administration', color='#e2e8f0',
fontsize=13, fontweight='bold')
ax.tick_params(colors='#94a3b8')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#334155')
ax.spines['bottom'].set_color('#334155')
plt.tight_layout()
plt.savefig('timeline.png', dpi=150, bbox_inches='tight', facecolor='#1a1a2e')
4. Forest Plot (Effect Size)
Use for: meta-analysis summaries, comparing multiple studies.
import matplotlib.pyplot as plt
import numpy as np
studies = ['Xu et al. (2016)', 'Caddy et al. (2014)', 'Fond et al. (2014)',
'McGirr et al. (2015)', 'Kishimoto et al. (2016)']
effects = [0.91, 0.82, 0.78, 0.95, 0.71]
cis = [(0.70, 1.12), (0.55, 1.09), (0.52, 1.04), (0.68, 1.22), (0.48, 0.94)]
fig, ax = plt.subplots(figsize=(9, 5))
fig.patch.set_facecolor('#1a1a2e')
ax.set_facecolor('#1a1a2e')
y_positions = range(len(studies))
for i, (effect, (lo, hi)) in enumerate(zip(effects, cis)):
color = '#6366f1' if lo > 0.5 else '#64748b'
ax.errorbar(effect, i, xerr=[[effect - lo], [hi - effect]],
fmt='o', color=color, capsize=3, capthick=1.5, markersize=8)
ax.text(hi + 0.03, i, f'd={effect:.2f}', va='center', color='#e2e8f0', fontsize=9)
ax.axvline(x=0, color='#475569', linewidth=1)
ax.axvline(x=0.5, color='#f59e0b', linestyle='--', alpha=0.5)
ax.axvline(x=0.8, color='#10b981', linestyle='--', alpha=0.5)
ax.set_yticks(y_positions)
ax.set_yticklabels(studies, color='#e2e8f0', fontsize=10)
ax.set_xlabel("Cohen's d (effect size)", color='#94a3b8', fontsize=11)
ax.set_title('Ketamine Meta-Analysis: Effect Sizes', color='#e2e8f0',
fontsize=14, fontweight='bold')
ax.tick_params(colors='#94a3b8')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_color('#334155')
ax.spines['bottom'].set_color('#334155')
plt.tight_layout()
plt.savefig('forest_plot.png', dpi=150, bbox_inches='tight', facecolor='#1a1a2e')
Design Standards
| Property | Value |
|---|---|
| Background | #1a1a2e (dark navy) |
| Primary color | #6366f1 (indigo) |
| Accent colors | #ec4899, #10b981, #f59e0b, #3b82f6, #ef4444 |
| Text color | #e2e8f0 (headings), #94a3b8 (labels), #64748b (captions) |
| Font | DejaVu Sans (headings), DejaVu Sans Mono (numbers) |
| DPI | 150 (2x scaling for Substack) |
| Spine color | #334155 |
| Grid | #94a3b8 at 10% opacity |
Never Do
- Pie charts with more than 5 slices
- 3D charts (they distort data)
- Dual y-axes without clear labeling on both sides
- Rainbow/disco color schemes
- Charts without source attribution in caption
PITFALLS
- matplotlib needs
Aggbackend in headless mode. Always setmatplotlib.use('Agg')before importing pyplot. - DPI matters for Substack. Substack compresses images. Generate at 150 DPI minimum, upload at 2x the intended display width.
- Font rendering differs on WSL. If fonts look wrong, test with
fc-list | grep -i dejavuto verify DejaVu fonts are installed.