# Scientific Charts

> Scientific Charts

- Skill: `lucadominguez/scientific-charts` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lucadominguez/scientific-charts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucadominguez/scientific-charts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lucadominguez (https://skillmd.com/u/lucadominguez)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucadominguez/scientific-charts

---

# 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

```bash
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.

```python
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.

```python
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.

```python
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.

```python
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

1. **matplotlib needs `Agg` backend in headless mode.** Always set `matplotlib.use('Agg')` before importing pyplot.
2. **DPI matters for Substack.** Substack compresses images. Generate at 150 DPI minimum, upload at 2x the intended display width.
3. **Font rendering differs on WSL.** If fonts look wrong, test with `fc-list | grep -i dejavu` to verify DejaVu fonts are installed.
