---
name: matplotlib-diagrams
description: Polished flowcharts, roadmaps, and technical diagrams with matplotlib patches.
version: 1
category: creative
triggers:
  - matplotlib flowchart
  - matplotlib roadmap
  - 技术路线图
  - python diagram
  - academic figure
  - FancyArrowPatch
  - FancyBboxPatch
  - matplotlib 流程图
tags: [matplotlib, diagram, flowchart, visualization, academic]
---

# Matplotlib Diagrams

Create polished flowcharts, roadmaps, and technical diagrams using matplotlib patches.

## When to Use

- Academic paper figures (技术路线图, system architecture)
- Flowcharts with boxes and arrows
- Any diagram where you need precise control over shapes and styling

## Key Components

### Boxes with Shadows

```python
from matplotlib.patches import FancyBboxPatch

# Shadow first (offset + lighter color)
shadow = FancyBboxPatch((x + 0.06, y - 0.06), width, height,
    boxstyle="round,pad=0.02,rounding_size=0.2",
    facecolor='#D0D0D0', edgecolor='none', alpha=0.5)
ax.add_patch(shadow)

# Main box on top
box = FancyBboxPatch((x, y), width, height,
    boxstyle="round,pad=0.02,rounding_size=0.2",
    facecolor=color, edgecolor='#888888', linewidth=1.2)
ax.add_patch(box)
```

### Simple Annotate Arrows (Recommended Default)

Clean arrows using ax.annotate — easier to use, consistent sizing:

```python
def draw_arrow(ax, x1, y1, x2, y2, lw=2.8):
    ax.annotate('', xy=(x2, y2), xytext=(x1, y1),
        arrowprops=dict(arrowstyle='-|>,head_length=0.6,head_width=0.35', 
                        color='#444444', lw=lw))
```

Key parameters:
- `head_length=0.6, head_width=0.35` — visible but not oversized
- `lw=2.8` — thick enough to be clear, not too heavy
- `-|>` style — more rounded than default `->`

### Capsule-Style Arrows (FancyArrowPatch)

For more control over fill/edge colors:

```python
from matplotlib.patches import FancyArrowPatch

def draw_arrow(ax, x1, y1, x2, y2):
    arrow = FancyArrowPatch(
        (x1, y1), (x2, y2),
        arrowstyle="Simple,head_length=10,head_width=8,tail_width=4",
        facecolor='#5D6D7E',
        edgecolor='#3D4D5E',
        linewidth=1.0,
        zorder=5
    )
    ax.add_patch(arrow)
```

### Fork/Branch Arrows (汇总箭头)

One source splitting to multiple targets — common for title→stages.

**Basic version (straight corners):**

```python
def draw_fork_arrows(ax, x_center, y_top, x_targets, y_bottom, lw=2.8):
    """One source splits to multiple targets via horizontal bar.
    IMPORTANT: lw must match your other arrows exactly!"""
    y_mid = (y_top + y_bottom) / 2 + 0.1
    
    # Vertical trunk from source
    ax.plot([x_center, x_center], [y_top, y_mid], 
            color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
    
    # Horizontal bar connecting all branches
    ax.plot([min(x_targets), max(x_targets)], [y_mid, y_mid],
            color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
    
    # Branch arrows to each target
    for x in x_targets:
        ax.plot([x, x], [y_mid, y_bottom + 0.15],
                color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
        ax.annotate('', xy=(x, y_bottom), xytext=(x, y_bottom + 0.15),
            arrowprops=dict(arrowstyle='-|>,head_length=0.6,head_width=0.35', 
                            color='#444444', lw=lw))
```

**Rounded corners version (bezier curves):**

```python
def draw_fork_arrows(ax, x_center, y_top, x_targets, y_bottom, lw=2.8):
    """Fork arrows with rounded corners using bezier curves."""
    from matplotlib.path import Path
    import matplotlib.patches as mpatches
    
    y_mid = (y_top + y_bottom) / 2 + 0.1
    r = 0.25  # Corner radius — increase for more visible rounding
    
    # Vertical trunk
    ax.plot([x_center, x_center], [y_top, y_mid], 
            color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
    
    for x in x_targets:
        if abs(x - x_center) < 0.1:
            # Center branch — straight down
            ax.plot([x, x], [y_mid, y_bottom + 0.15],
                    color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
        else:
            # Horizontal segment (leave room for corner)
            if x < x_center:
                ax.plot([x_center, x + r], [y_mid, y_mid],
                        color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
                verts = [(x + r, y_mid), (x, y_mid), (x, y_mid - r)]
            else:
                ax.plot([x_center, x - r], [y_mid, y_mid],
                        color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
                verts = [(x - r, y_mid), (x, y_mid), (x, y_mid - r)]
            
            # Bezier curve for rounded corner
            codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
            path = Path(verts, codes)
            patch = mpatches.PathPatch(path, facecolor='none', edgecolor='#444444', 
                                       linewidth=lw, capstyle='round', zorder=4)
            ax.add_patch(patch)
            
            # Vertical segment below corner
            ax.plot([x, x], [y_mid - r, y_bottom + 0.15],
                    color='#444444', linewidth=lw, solid_capstyle='round', zorder=4)
        
        # Arrow head
        ax.annotate('', xy=(x, y_bottom), xytext=(x, y_bottom + 0.15),
            arrowprops=dict(arrowstyle='-|>,head_length=0.6,head_width=0.35', 
                            color='#444444', lw=lw))
```

### Dual-Layer Arrows (Optional)

Outer glow + inner solid — can look too heavy, use sparingly:

```python
ARROW_MAIN = '#5D6D7E'
ARROW_LIGHT = '#8899AA'

# Outer layer (glow)
outer = FancyArrowPatch((x, y1), (x, y2),
    arrowstyle="Simple,head_length=18,head_width=16,tail_width=10",
    facecolor=ARROW_LIGHT, edgecolor='none', alpha=0.5, zorder=1)

# Inner layer (main)
inner = FancyArrowPatch((x, y1), (x, y2),
    arrowstyle="Simple,head_length=14,head_width=12,tail_width=6",
    facecolor=ARROW_MAIN, edgecolor='#4D5D6E', linewidth=0.5, zorder=2)
```

### Chinese Font Setup

**Preferred method** — use FontProperties with explicit path (avoids font-not-found warnings):

```python
import matplotlib.font_manager as fm

# Find available Chinese font
font_path = '/usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc'  # Ubuntu server
font_prop = fm.FontProperties(fname=font_path)
plt.rcParams['axes.unicode_minus'] = False

# Apply to text elements
ax.set_xlabel('预测标签', fontsize=14, fontproperties=font_prop)
ax.set_ylabel('真实标签', fontsize=14, fontproperties=font_prop)
ax.text(x, y, '中文文本', fontproperties=font_prop)
plt.legend(prop=font_prop)  # Legend needs 'prop' not 'fontproperties'
```

**Fallback method** — rcParams (may produce font warnings):

```python
plt.rcParams['font.sans-serif'] = ['Noto Serif CJK SC', 'SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
```

**Find available Chinese fonts**:
```bash
fc-list :lang=zh | head -5
```

### Y-Axis Label Orientation

For academic figures, user may prefer **horizontal labels** (easier to read) over the default rotated style:

```python
# Horizontal Y-axis label (rotation=0)
ax.set_ylabel('近端约束系数 μ', fontsize=12, fontproperties=font_prop, 
              rotation=0, labelpad=60)  # labelpad pushes it left to avoid overlap

# Or use ax.text for precise positioning
ax.text(-8, 0.06, '近端约束系数 μ', fontsize=12, fontproperties=font_prop,
        ha='center', va='center', rotation=0)
```

**Pitfall**: When using `rotation=0`, you need `labelpad` to prevent the label from overlapping the tick labels. Start with `labelpad=40-60` and adjust.

### Chinese Vertical Text (竖排文字)

For academic figures, user may prefer **vertical stacking** — each character on its own line, all upright (not rotated):

```python
# Vertical Y-axis label — one character per line
ax.set_ylabel('近\n端\n约\n束\n系\n数\n︵\nμ\n︶', fontsize=12, fontproperties=font_prop, 
              rotation=0, labelpad=20, linespacing=0.8, va='center')

# Colorbar label — same technique
cbar.set_label('样\n本\n数\n量\n︵\n张\n︶', fontsize=12, fontproperties=font_prop,
               rotation=0, labelpad=25, linespacing=0.8, va='center')
```

**Key points**:
- `rotation=0` — keeps each character upright (正着的)
- `\n` between each character — creates vertical stack
- `linespacing=0.8` — tightens the vertical spacing
- `va='center'` — centers the label along the axis

**Vertical brackets** — use special Unicode characters for proper orientation:
- `︵` (U+FE35) — vertical left parenthesis (opening)
- `︶` (U+FE36) — vertical right parenthesis (closing)
- Regular `（）` will look wrong when stacked vertically

**Pitfall**: Vertical bracket characters may appear slightly smaller than surrounding Chinese characters due to font rendering. If user notices size inconsistency, consider using regular parentheses with rotation or adjusting font size for brackets specifically.

## Academic Color Palettes

```python
# Soft academic (粉蓝配色)
PINK = '#FFB6C1'
BLUE = '#AED6F1'
METRIC_BOX = '#FFD54F'  # Golden for highlights
STAGE_BG = '#F8F9FA'
LABEL_BG = '#E8E8E8'
```

## Pitfalls

1. **Arrow zorder matters** — outer layer zorder=1, inner zorder=2
2. **Dashed borders** — use `linestyle='--'` on FancyBboxPatch
3. **Save with white background** — `facecolor='white'` in savefig
4. **DPI for print** — use dpi=150+ for academic papers
5. **Minimal iteration** — when user says "只改X其他不变", copy the working version and ONLY modify X. Don't regenerate from scratch.
6. **Dual-layer arrows can be too heavy** — user may reject as "太花" or "完全不行". Start with simple capsule arrows, offer dual-layer as option.
7. **Save working versions** — before iterating, save current best to `~/.hermes/scripts/` so you can revert
8. **Visual consistency across arrow types** — when adding fork arrows alongside existing simple arrows, match the line width (lw) exactly. User will reject if fork lines are thinner OR thicker than other arrows. Check existing arrow lw first, use same value for fork lines.
9. **Arrow head sizing** — default matplotlib arrows are too small. Use `head_length=0.6, head_width=0.35` for visible but not oversized heads. User iterated through 0.4→0.6 to find this.
10. **Line width sweet spot** — lw=2.8 works well for academic diagrams. Too thin (1.8) looks weak, too thick (6+) looks heavy.
11. **Rounded corners need visible radius** — r=0.25 is minimum visible. r=0.12-0.15 looks like straight corners. If user asks for rounded corners, start with r=0.25+.
12. **Iterative arrow refinement** — when user says arrows are "普通" or "有点普通", offer: (a) fork/branch arrows for hierarchy, (b) rounded corners, (c) thicker lines, (d) larger arrow heads. Don't change everything at once — iterate one parameter at a time.
13. **Selective rounded corners** — user may want only outer branches (leftmost/rightmost) rounded, middle branches straight. Check `is_leftmost = (x == min(x_targets))` and `is_rightmost = (x == max(x_targets))` to apply corners selectively.
14. **Horizontal bar must stop at corner radius** — when drawing fork arrows with rounded corners, the horizontal line must end at `x + r` (left) or `x - r` (right), NOT extend to the full x position. Otherwise the line visibly overshoots the corner curve. User will catch this: "横线突出圆角了".

## Sequence Diagrams (时序图)

When Mermaid can't render proper math notation (subscripts/superscripts), use matplotlib instead.

```python
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
plt.rcParams['mathtext.fontset'] = 'cm'  # LaTeX-style math font

# Participant boxes
def draw_participant(ax, x, y, label, sublabel):
    box = FancyBboxPatch((x-1.2, y-0.4), 2.4, 0.8,
                         boxstyle="round,pad=0.05,rounding_size=0.1",
                         facecolor='#E8F4FD', edgecolor='#3B82F6', linewidth=2)
    ax.add_patch(box)
    ax.text(x, y+0.05, label, ha='center', va='center', fontsize=11, fontweight='bold')
    ax.text(x, y-0.25, sublabel, ha='center', va='center', fontsize=9, color='#666666')

# Lifelines (dashed vertical)
ax.plot([x, x], [top_y, bottom_y], color='#CCCCCC', linewidth=1.5, linestyle='--')

# Message arrows with LaTeX labels
ax.annotate('', xy=(x2, y), xytext=(x1, y),
            arrowprops=dict(arrowstyle='->', color='#3B82F6', lw=2))
ax.text(mid_x, y+0.25, r'广播参数 $w_t$', ha='center')  # LaTeX subscript

# Dashed return arrows
ax.annotate('', xy=(x2, y), xytext=(x1, y),
            arrowprops=dict(arrowstyle='->', color='#3B82F6', lw=2, linestyle='--'))

# Note boxes (yellow)
box = FancyBboxPatch((x-w/2, y-h/2), w, h,
                     boxstyle="round,pad=0.05,rounding_size=0.08",
                     facecolor='#FFFBEB', edgecolor='#F59E0B', linewidth=1.2)

# Background rect for iteration scope
rect = FancyBboxPatch((x, y), w, h,
                      boxstyle="round,pad=0.05,rounding_size=0.15",
                      facecolor='#EAF3FF', edgecolor='#93C5FD', linewidth=1.5, alpha=0.5)
```

### LaTeX Math Notation

```python
# Subscript only (no parentheses)
r'$w_t$'           # w with subscript t
r'$w_{t+1}$'       # w with subscript t+1

# Subscript + superscript
r'$w_1^{t+1}$'     # w with subscript 1, superscript t+1
r'$w_K^{t+1}$'     # w with subscript K, superscript t+1

# With parentheses (if needed)
r'$w^{(t)}$'       # w with superscript (t)
```

**Pitfalls**:
- Confirm notation style with user — some prefer `w_t` (subscript), others `w^{(t)}` (superscript with parens). Don't assume — ask "你论文里实际用的符号是什么样的？" before generating.
- **Lifeline length** — dashed vertical lines must NOT extend below the bottom element (e.g., aggregation box). Set the endpoint to stop at or slightly above the lowest box. User will catch: "虚线稍微超出了".

## Confusion Matrix (混淆矩阵)

Complete example with consistent fonts across all elements:

```python
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import numpy as np

# Use Regular (not Bold) for cleaner academic look
font_path = '/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc'
font_prop = fm.FontProperties(fname=font_path)
plt.rcParams['axes.unicode_minus'] = False

data = np.array([
    [231, 17, 2],
    [21, 232, 2],
    [1, 0, 249]
])
labels = ['小麦', '水稻', '玉米']

fig, ax = plt.subplots(figsize=(8, 7))
im = ax.imshow(data, cmap='Blues')

ax.set_xticks(np.arange(len(labels)))
ax.set_yticks(np.arange(len(labels)))
ax.set_xticklabels(labels, fontsize=14, fontproperties=font_prop)
ax.set_yticklabels(labels, fontsize=14, fontproperties=font_prop)

ax.set_xlabel('预测标签', fontsize=14, fontproperties=font_prop)
# Vertical stacked label with proper brackets
ax.set_ylabel('真\n实\n标\n签', fontsize=14, fontproperties=font_prop, 
              rotation=0, labelpad=15, linespacing=0.8, va='center')

# Matrix cell values — use default font to match colorbar ticks
for i in range(len(labels)):
    for j in range(len(labels)):
        value = data[i, j]
        color = 'white' if value > 150 else 'black'
        # NOTE: Do NOT use fontweight='bold' or fontproperties here
        # Default matplotlib font matches colorbar tick numbers
        ax.text(j, i, str(value), ha='center', va='center', 
                fontsize=18, color=color)

cbar = plt.colorbar(im, ax=ax, shrink=0.8)
# IMPORTANT: Match fontsize with Y-axis label (both 14)
cbar.set_label('样\n本\n数\n量\n︵\n张\n︶', fontsize=14, fontproperties=font_prop, 
               rotation=0, labelpad=25, linespacing=0.8, va='center')

# CRITICAL: Set colorbar tick label font to match everything else
cbar.ax.tick_params(labelsize=12)
for label in cbar.ax.get_yticklabels():
    label.set_fontproperties(font_prop)

plt.tight_layout()
plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')
```

**Pitfalls for confusion matrix**:
1. **Matrix cell vs colorbar font mismatch** — If user wants matrix numbers (231, 17, etc.) to match colorbar tick numbers (0, 50, 100), do NOT use `fontweight='bold'` or custom `fontproperties` on cell text. Just use default matplotlib font. User will notice: "数字字体是不是不一致".
2. **Colorbar tick font mismatch** — By default, colorbar ticks use matplotlib's default font (DejaVu Sans), not your custom Chinese font. If you want Chinese labels on colorbar, must explicitly set `cbar.ax.tick_params()` and loop through `cbar.ax.get_yticklabels()` to apply fontproperties.
3. **Font size consistency** — Keep Y-axis label, X-axis label, and colorbar label at the same fontsize (e.g., all 14). User will notice if colorbar is smaller: "字体大小是不是一致".
4. **Label spacing alignment** — User may want "真实标签→水稻" distance to match "预测标签→水稻" distance. Adjust `labelpad` (smaller = closer to axis).
5. **Bold vs Regular** — Noto Serif CJK Bold can look too heavy. Try Regular first: `/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc`
6. **Verify data matches paper** — Before generating, calculate accuracy from confusion matrix (sum of diagonal / total) and confirm it matches user's paper. User will catch: "这里面的数据符不符合我的论文".
7. **Title should NOT include accuracy** — Academic confusion matrix titles typically show only model/dataset info (e.g., "混淆矩阵 (Crop-ResNet18)"), NOT the accuracy percentage. User corrected: "不需要额外标出准确率，混淆矩阵应该不在图题里面标出这个的".
8. **Vertical label with correct orientation** — For Y-axis label, use newlines to stack vertically but `rotation=0` to keep characters upright (not sideways). Example: `ax.set_ylabel('真\n实\n标\n签', rotation=0, labelpad=10, va='center', fontsize=12)`.
9. **Label-to-axis spacing consistency** — User expects "真实标签→小麦" distance to equal "预测标签→小麦" distance. Adjust `labelpad` — smaller values bring label closer. Start with `labelpad=10` and iterate based on visual feedback.
10. **Search memory before regenerating** — If user asks to regenerate a figure they've seen before (e.g., "混淆矩阵怎么弄的"), FIRST search local memory for the previous version's format (language, labels, style). Don't assume — user corrected: "不是英文的呀，怎么你现在的变英文的了". The previous version's choices (Chinese vs English labels, 3 vs 5 classes, etc.) are intentional.
10. **Search memory before regenerating** — If user asks to regenerate a figure they've seen before (e.g., "混淆矩阵怎么弄的"), FIRST search local memory for the previous version's format (language, labels, style). Don't assume — user corrected: "不是英文的呀，怎么你现在的变英文的了". The previous version's choices (Chinese vs English labels, 3 vs 5 classes, etc.) are intentional.

## Support Files

- `templates/roadmap_template.py` — Full 5-stage roadmap example
- `templates/sequence_diagram.py` — Federated learning sequence diagram with LaTeX math
- `templates/confusion_matrix.py` — Confusion matrix with consistent Chinese fonts
- `references/chinese-confusion-matrix.md` — 中文混淆矩阵完整示例（竖排标签、竖排括号 ︵︶、colorbar 格式）
