Create publication-quality scientific diagrams, flowcharts, and schematics using Python (graphviz, matplotlib, schemdraw, networkx). Specialized in neural network architectures, system diagrams, and flowcharts. Generates SVG/EPS in figures/ folder with automated quality verification.
Scientific schematics and diagrams transform complex concepts into clear visual representations for publication. Generate neural network architectures, flowcharts, circuit diagrams, biological pathways, and system diagrams using best-in-class Python libraries. All diagrams are created as SVG/EPS files, stored in the figures/ subfolder, and referenced in papers/posters - never embedded directly in LaTeX.
Standard workflow for ALL diagrams:
figures/ subfolder with descriptive name\includegraphics{figures/diagram_name.pdf} in LaTeXKey principle: Generate standalone vector graphics first, then integrate into documents.
This skill should be used when:
Choose the optimal library for your specific diagram type:
Best library: graphviz via Python's pygraphviz or pydot
Alternative: Custom matplotlib with careful positioning
Best library: graphviz with dot or flowchart layout
Alternative: diagrams library (for cloud/system architecture style)
Best library: schemdraw
Best library: networkx with custom rendering
Best library: graphviz or diagrams
Creating a Transformer encoder-decoder diagram like in "Attention Is All You Need":
import graphviz
from pathlib import Path
def create_transformer_diagram(output_dir='figures'):
"""
Create a Transformer architecture diagram.
Zero-shot generation with automatic layout.
"""
Path(output_dir).mkdir(exist_ok=True)
# Create directed graph with TB (top-to-bottom) layout
dot = graphviz.Digraph(
'transformer',
format='pdf',
graph_attr={
'rankdir': 'BT', # Bottom to top (like the original paper)
'splines': 'ortho', # Orthogonal edges
'nodesep': '0.5',
'ranksep': '0.8',
'bgcolor': 'white',
'dpi': '300'
},
node_attr={
'shape': 'box',
'style': 'rounded,filled',
'fillcolor': 'lightgray',
'fontname': 'Arial',
'fontsize': '11',
'width': '2.5',
'height': '0.5'
},
edge_attr={
'color': 'black',
'penwidth': '1.5'
}
)
# ENCODER STACK (left side)
with dot.subgraph(name='cluster_encoder') as enc:
enc.attr(label='Encoder', fontsize='14', fontname='Arial-Bold')
enc.attr(style='rounded', color='blue', penwidth='2')
# Encoder layers (bottom to top)
enc.node('enc_input_emb', 'Input Embedding', fillcolor='#E8F4F8')
enc.node('enc_pos', 'Positional Encoding', fillcolor='#E8F4F8')
enc.node('enc_mha', 'Multi-Head\nAttention', fillcolor='#B3D9E6')
enc.node('enc_an1', 'Add & Norm', fillcolor='#CCE5FF')
enc.node('enc_ff', 'Feed Forward', fillcolor='#B3D9E6')
enc.node('enc_an2', 'Add & Norm', fillcolor='#CCE5FF')
# Encoder flow
enc.edge('enc_input_emb', 'enc_pos')
enc.edge('enc_pos', 'enc_mha')
enc.edge('enc_mha', 'enc_an1')
enc.edge('enc_an1', 'enc_ff')
enc.edge('enc_ff', 'enc_an2')
# DECODER STACK (right side)
with dot.subgraph(name='cluster_decoder') as dec:
dec.attr(label='Decoder', fontsize='14', fontname='Arial-Bold')
dec.attr(style='rounded', color='red', penwidth='2')
# Decoder layers (bottom to top)
dec.node('dec_output_emb', 'Output Embedding', fillcolor='#FFE8E8')
dec.node('dec_pos', 'Positional Encoding', fillcolor='#FFE8E8')
dec.node('dec_mmha', 'Masked Self-\nAttention', fillcolor='#FFB3B3')
dec.node('dec_an1', 'Add & Norm', fillcolor='#FFCCCC')
dec.node('dec_cross', 'Cross-Attention', fillcolor='#FFB3B3')
dec.node('dec_an2', 'Add & Norm', fillcolor='#FFCCCC')
dec.node('dec_ff', 'Feed Forward', fillcolor='#FFB3B3')
dec.node('dec_an3', 'Add & Norm', fillcolor='#FFCCCC')
dec.node('dec_linear', 'Linear & Softmax', fillcolor='#FF9999')
dec.node('dec_output', 'Output\nProbabilities', fillcolor='#FFE8E8')
# Decoder flow
dec.edge('dec_output_emb', 'dec_pos')
dec.edge('dec_pos', 'dec_mmha')
dec.edge('dec_mmha', 'dec_an1')
dec.edge('dec_an1', 'dec_cross')
dec.edge('dec_cross', 'dec_an2')
dec.edge('dec_an2', 'dec_ff')
dec.edge('dec_ff', 'dec_an3')
dec.edge('dec_an3', 'dec_linear')
dec.edge('dec_linear', 'dec_output')
# Cross-attention connection (encoder to decoder)
dot.edge('enc_an2', 'dec_cross',
style='dashed',
color='purple',
label=' context ',
fontsize='9')
# Input and output labels
dot.node('input_seq', 'Input Sequence',
shape='ellipse', fillcolor='lightgreen')
dot.node('target_seq', 'Target Sequence',
shape='ellipse', fillcolor='lightgreen')
dot.edge('input_seq', 'enc_input_emb')
dot.edge('target_seq', 'dec_output_emb')
# Render to files
output_path = f'{output_dir}/transformer_architecture'
dot.render(output_path, cleanup=True)
# Also save as SVG and EPS
dot.format = 'svg'
dot.render(output_path, cleanup=True)
dot.format = 'eps'
dot.render(output_path, cleanup=True)
print(f"✓ Transformer diagram created:")
print(f" - {output_path}.pdf")
print(f" - {output_path}.svg")
print(f" - {output_path}.eps")
return f"{output_path}.pdf"
# Usage
if __name__ == '__main__':
diagram_path = create_transformer_diagram('figures')
# Run quality checks
from quality_checker import run_quality_checks
run_quality_checks(diagram_path.replace('.pdf', '.png'))
LaTeX integration:
\begin{figure}[htbp]
\centering
\includegraphics[width=0.9\textwidth]{figures/transformer_architecture.pdf}
\caption{Transformer encoder-decoder architecture showing multi-head attention,
feed-forward layers, and cross-attention mechanism.}
\label{fig:transformer}
\end{figure}
import graphviz
from pathlib import Path
def create_consort_flowchart(output_dir='figures'):
"""Create a CONSORT participant flow diagram."""
Path(output_dir).mkdir(exist_ok=True)
dot = graphviz.Digraph(
'consort',
format='pdf',
graph_attr={
'rankdir': 'TB',
'splines': 'ortho',
'nodesep': '0.6',
'ranksep': '0.8',
'bgcolor': 'white'
},
node_attr={
'shape': 'box',
'style': 'rounded,filled',
'fillcolor': '#E8F4F8',
'fontname': 'Arial',
'fontsize': '10',
'width': '3',
'height': '0.6'
}
)
# Enrollment
dot.node('assessed', 'Assessed for eligibility\n(n=500)')
dot.node('excluded', 'Excluded (n=150)\n• Age < 18: n=80\n• Declined: n=50\n• Other: n=20')
dot.node('randomized', 'Randomized\n(n=350)')
# Allocation
dot.node('treatment', 'Allocated to treatment\n(n=175)', fillcolor='#C8E6C9')
dot.node('control', 'Allocated to control\n(n=175)', fillcolor='#FFECB3')
# Follow-up
dot.node('treat_lost', 'Lost to follow-up (n=15)', fillcolor='#FFCDD2')
dot.node('ctrl_lost', 'Lost to follow-up (n=10)', fillcolor='#FFCDD2')
# Analysis
dot.node('treat_analyzed', 'Analyzed (n=160)', fillcolor='#C8E6C9')
dot.node('ctrl_analyzed', 'Analyzed (n=165)', fillcolor='#FFECB3')
# Connect nodes
dot.edge('assessed', 'excluded')
dot.edge('assessed', 'randomized')
dot.edge('randomized', 'treatment')
dot.edge('randomized', 'control')
dot.edge('treatment', 'treat_lost')
dot.edge('treatment', 'treat_analyzed')
dot.edge('control', 'ctrl_lost')
dot.edge('control', 'ctrl_analyzed')
# Render
output_path = f'{output_dir}/consort_flowchart'
dot.render(output_path, cleanup=True)
print(f"✓ CONSORT flowchart created: {output_path}.pdf")
return f"{output_path}.pdf"
def create_cnn_architecture(output_dir='figures'):
"""Create a CNN architecture diagram."""
dot = graphviz.Digraph(
'cnn',
format='pdf',
graph_attr={'rankdir': 'LR', 'bgcolor': 'white'}
)
# Define layers
layers = [
('input', 'Input\n32×32×3', '#FFE8E8'),
('conv1', 'Conv 3×3\n32 filters', '#B3D9E6'),
('pool1', 'MaxPool\n2×2', '#FFE5B3'),
('conv2', 'Conv 3×3\n64 filters', '#B3D9E6'),
('pool2', 'MaxPool\n2×2', '#FFE5B3'),
('flatten', 'Flatten', '#D4E8D4'),
('fc1', 'FC 128', '#C8B3E6'),
('fc2', 'FC 10', '#C8B3E6'),
('softmax', 'Softmax', '#FFC8C8')
]
# Create nodes
for node_id, label, color in layers:
dot.node(node_id, label,
shape='box', style='rounded,filled',
fillcolor=color, fontname='Arial')
# Connect layers
for i in range(len(layers) - 1):
dot.edge(layers[i][0], layers[i+1][0])
output_path = f'{output_dir}/cnn_architecture'
dot.render(output_path, cleanup=True)
print(f"✓ CNN diagram created: {output_path}.pdf")
return f"{output_path}.pdf"
Neural Network Architectures
Methodology Flowcharts
Circuit Diagrams
Biological Diagrams
System Architecture Diagrams
Graphviz is the best tool for most scientific diagrams due to automatic layout, clean rendering, and zero-overlap guarantee.
Installation:
# Install Graphviz binary (required)
# macOS
brew install graphviz
# Ubuntu/Debian
sudo apt-get install graphviz
# Install Python bindings
pip install graphviz
Why Graphviz is optimal:
Schemdraw - Circuit diagrams only
pip install schemdraw
NetworkX - Complex network analysis + visualization
pip install networkx matplotlib
Matplotlib - Custom manual diagrams (when you need exact control)
pip install matplotlib
Follow this systematic approach for any diagram type:
Ask yourself:
rankdir='TB' or 'BT' (top-to-bottom or bottom-to-top)rankdir='LR' (left-to-right)Start with this template and customize:
import graphviz
from pathlib import Path
def create_diagram(output_dir='figures', diagram_name='my_diagram'):
"""Universal diagram creation template."""
Path(output_dir).mkdir(exist_ok=True, parents=True)
dot = graphviz.Digraph(
name=diagram_name,
format='pdf',
graph_attr={
'rankdir': 'TB', # TB, BT, LR, or RL
'splines': 'ortho', # ortho (straight) or curved
'nodesep': '0.6', # horizontal spacing
'ranksep': '0.8', # vertical spacing
'bgcolor': 'white',
'dpi': '300'
},
node_attr={
'shape': 'box', # box, ellipse, diamond, etc.
'style': 'rounded,filled',
'fillcolor': 'lightgray',
'fontname': 'Arial',
'fontsize': '11',
'margin': '0.2',
'width': '2', # minimum width
'height': '0.5' # minimum height
},
edge_attr={
'color': 'black',
'penwidth': '1.5',
'arrowsize': '0.8'
}
)
# Add your nodes and edges here
dot.node('node1', 'Label 1')
dot.node('node2', 'Label 2')
dot.edge('node1', 'node2')
# Render to multiple formats
output_path = f'{output_dir}/{diagram_name}'
dot.render(output_path, cleanup=True) # PDF
dot.format = 'svg'
dot.render(output_path, cleanup=True) # SVG
dot.format = 'eps'
dot.render(output_path, cleanup=True) # EPS
print(f"✓ Diagram saved: {output_path}.{{pdf,svg,eps}}")
return f"{output_path}.pdf"
Best practices:
'encoder_layer1' not 'n1'\n for multi-line labels# Good node definitions
dot.node('input_layer', 'Input Layer\n(512 dims)', fillcolor='#E8F4F8')
dot.node('attention', 'Multi-Head\nAttention', fillcolor='#B3D9E6')
dot.node('output', 'Output', fillcolor='#C8E6C9')
Edge types:
# Standard arrow
dot.edge('node1', 'node2')
# Dashed line (for information flow)
dot.edge('encoder', 'decoder', style='dashed')
# Bidirectional
dot.edge('node1', 'node2', dir='both')
# With label
dot.edge('layer1', 'layer2', label=' ReLU ')
# Different color
dot.edge('input', 'output', color='red', penwidth='2')
For parallel structures (like Encoder/Decoder):
# Encoder cluster
with dot.subgraph(name='cluster_encoder') as enc:
enc.attr(label='Encoder', style='rounded', color='blue')
enc.node('enc1', 'Encoder Layer 1')
enc.node('enc2', 'Encoder Layer 2')
enc.edge('enc1', 'enc2')
# Decoder cluster
with dot.subgraph(name='cluster_decoder') as dec:
dec.attr(label='Decoder', style='rounded', color='red')
dec.node('dec1', 'Decoder Layer 1')
dec.node('dec2', 'Decoder Layer 2')
dec.edge('dec1', 'dec2')
# Cross-connection between clusters
dot.edge('enc2', 'dec1', style='dashed', color='purple')
# Always render to PDF (for LaTeX) and SVG (for web/slides)
output_path = f'{output_dir}/{diagram_name}'
# PDF for papers
dot.format = 'pdf'
dot.render(output_path, cleanup=True)
# SVG for posters/slides
dot.format = 'svg'
dot.render(output_path, cleanup=True)
# EPS for some journals
dot.format = 'eps'
dot.render(output_path, cleanup=True)
graph_attr={
'rankdir': 'TB', # Direction: TB, BT, LR, RL
'splines': 'ortho', # Edge style: ortho, curved, line, polyline
'nodesep': '0.5', # Space between nodes (inches)
'ranksep': '0.8', # Space between ranks (inches)
'bgcolor': 'white', # Background color
'dpi': '300', # Resolution for raster output
'compound': 'true', # Allow edges between clusters
'concentrate': 'true' # Merge multiple edges
}
node_attr={
'shape': 'box', # box, ellipse, circle, diamond, plaintext
'style': 'rounded,filled', # rounded, filled, dashed, bold
'fillcolor': '#E8F4F8', # Fill color (hex or name)
'color': 'black', # Border color
'penwidth': '1.5', # Border width
'fontname': 'Arial', # Font family
'fontsize': '11', # Font size (points)
'fontcolor': 'black', # Text color
'width': '2', # Minimum width (inches)
'height': '0.5', # Minimum height (inches)
'margin': '0.2' # Internal padding
}
edge_attr={
'color': 'black', # Line color
'penwidth': '1.5', # Line width
'style': 'solid', # solid, dashed, dotted, bold
'arrowsize': '1.0', # Arrow head size
'dir': 'forward', # forward, back, both, none
'arrowhead': 'normal' # normal, vee, diamond, dot, none
}
Use these color sets to ensure accessibility:
OKABE_ITO = {
'orange': '#E69F00',
'sky_blue': '#56B4E9',
'green': '#009E73',
'yellow': '#F0E442',
'blue': '#0072B2',
'vermillion': '#D55E00',
'purple': '#CC79A7',
'black': '#000000'
}
LIGHT_FILLS = {
'blue': '#E8F4F8',
'green': '#E8F5E9',
'orange': '#FFF3E0',
'purple': '#F3E5F5',
'red': '#FFEBEE',
'yellow': '#FFFDE7',
'gray': '#F5F5F5'
}
All diagrams follow scientific publication best practices:
Vector Format Output
Colorblind-Friendly Design
Typography Standards
Accessibility
For comprehensive publication guidelines, see references/best_practices.md.
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{shapes.geometric, arrows.meta}
% Load colorblind-safe colors
\input{tikz_styles.tex}
\begin{document}
\begin{figure}[h]
\centering
\begin{tikzpicture}[
node distance=2cm,
process/.style={rectangle, rounded corners, draw=black, thick,
fill=okabe-blue!20, minimum width=3cm, minimum height=1cm},
decision/.style={diamond, draw=black, thick, fill=okabe-orange!20,
minimum width=2cm, aspect=2},
arrow/.style={-Stealth, thick}
]
% Nodes
\node (start) [process] {Screen Participants\\(n=500)};
\node (exclude) [process, below of=start] {Exclude (n=150)\\Age $<$ 18 years};
\node (randomize) [process, below of=exclude] {Randomize (n=350)};
\node (treatment) [process, below left=1.5cm and 2cm of randomize]
{Treatment Group\\(n=175)};
\node (control) [process, below right=1.5cm and 2cm of randomize]
{Control Group\\(n=175)};
\node (analyze) [process, below=3cm of randomize] {Analyze Data};
% Arrows
\draw [arrow] (start) -- (exclude);
\draw [arrow] (exclude) -- (randomize);
\draw [arrow] (randomize) -| (treatment);
\draw [arrow] (randomize) -| (control);
\draw [arrow] (treatment) |- (analyze);
\draw [arrow] (control) |- (analyze);
\end{tikzpicture}
\caption{Study participant flow diagram following CONSORT guidelines.}
\label{fig:consort}
\end{figure}
\end{document}
import schemdraw
import schemdraw.elements as elm
# Create drawing with colorblind-safe colors
d = schemdraw.Drawing()
# Voltage source
d += elm.SourceV().label('$V_s$')
# Resistors in series
d += elm.Resistor().right().label('$R_1$\n1kΩ')
d += elm.Resistor().label('$R_2$\n2kΩ')
# Capacitor
d += elm.Capacitor().down().label('$C_1$\n10µF')
# Close the circuit
d += elm.Line().left().tox(d.elements[0].start)
# Add ground
d += elm.Ground()
# Save as vector graphics
d.save('circuit_diagram.svg')
d.save('circuit_diagram.pdf')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
# Okabe-Ito colorblind-safe palette
colors = {
'protein': '#56B4E9', # Blue
'gene': '#009E73', # Green
'process': '#F0E442', # Yellow
'inhibition': '#D55E00' # Orange
}
fig, ax = plt.subplots(figsize=(8, 6))
# Define proteins as rounded rectangles
proteins = [
('Receptor', 1, 5),
('Kinase A', 3, 5),
('Kinase B', 5, 5),
('TF', 7, 5),
('Gene', 7, 3)
]
for name, x, y in proteins:
color = colors['gene'] if name == 'Gene' else colors['protein']
box = FancyBboxPatch((x-0.4, y-0.3), 0.8, 0.6,
boxstyle="round,pad=0.1",
facecolor=color, edgecolor='black', linewidth=2)
ax.add_patch(box)
ax.text(x, y, name, ha='center', va='center', fontsize=10, fontweight='bold')
# Add activation arrows
arrows = [
(1.5, 5, 2.5, 5, 'black'), # Receptor -> Kinase A
(3.5, 5, 4.5, 5, 'black'), # Kinase A -> Kinase B
(5.5, 5, 6.5, 5, 'black'), # Kinase B -> TF
(7, 4.7, 7, 3.6, 'black') # TF -> Gene
]
for x1, y1, x2, y2, color in arrows:
arrow = FancyArrowPatch((x1, y1), (x2, y2),
arrowstyle='->', mutation_scale=20,
linewidth=2, color=color)
ax.add_patch(arrow)
# Configure axes
ax.set_xlim(0, 8.5)
ax.set_ylim(2, 6)
ax.set_aspect('equal')
ax.axis('off')
plt.tight_layout()
plt.savefig('signaling_pathway.pdf', bbox_inches='tight', dpi=300)
plt.savefig('signaling_pathway.png', bbox_inches='tight', dpi=300)
Follow this systematic workflow for all diagrams:
Identify diagram type - What are you visualizing?
Determine layout direction
rankdir='TB'rankdir='BT'rankdir='LR'rankdir='RL'Identify groupings
Standard procedure for 95% of diagrams:
import graphviz
from pathlib import Path
# 1. Set up output directory
output_dir = 'figures'
Path(output_dir).mkdir(exist_ok=True, parents=True)
# 2. Create diagram with base template
dot = graphviz.Digraph(
'my_diagram',
format='pdf',
graph_attr={
'rankdir': 'TB', # Adjust based on Phase 1
'splines': 'ortho', # Clean orthogonal edges
'nodesep': '0.6', # Good default spacing
'ranksep': '0.8',
'bgcolor': 'white',
'dpi': '300'
},
node_attr={
'shape': 'box',
'style': 'rounded,filled',
'fillcolor': 'lightgray',
'fontname': 'Arial',
'fontsize': '11'
},
edge_attr={'color': 'black', 'penwidth': '1.5'}
)
# 3. Add nodes (with descriptive IDs and clear labels)
dot.node('input', 'Input Layer', fillcolor='#E8F4F8')
dot.node('hidden', 'Hidden Layer', fillcolor='#B3D9E6')
dot.node('output', 'Output Layer', fillcolor='#C8E6C9')
# 4. Add edges
dot.edge('input', 'hidden')
dot.edge('hidden', 'output')
# 5. Render to figures/ folder
output_path = f'{output_dir}/my_diagram'
dot.render(output_path, cleanup=True) # Creates PDF
dot.format = 'svg'
dot.render(output_path, cleanup=True) # Creates SVG
dot.format = 'eps'
dot.render(output_path, cleanup=True) # Creates EPS
print(f"✓ Saved to: {output_path}.{{pdf,svg,eps}}")
Automatic checks:
# Convert PDF to PNG for quality checking
from pdf2image import convert_from_path
pages = convert_from_path(f'{output_path}.pdf', dpi=300)
pages[0].save(f'{output_path}.png')
# Run quality checks
from quality_checker import run_quality_checks
report = run_quality_checks(f'{output_path}.png')
if report['overall_status'] != 'PASS':
print("⚠️ Issues detected - review quality_reports/")
# Adjust spacing: increase nodesep or ranksep
# Adjust colors: check accessibility report