January 16, 2026
Alice Brunner
Language: Python
#!pip3 install pandas openpyxl
# Assembly synteny + coverage figure
import pandas as pd # pandas 1.5.3 # numpy backend 1.26.0
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch
from Bio import SeqIO
import os
import re
The following input is required: Data/Coverage_Isolate_against_Isolate.txt.gz
# Generate folder of per-contig coverage
df = pd.read_csv('Data/Coverage_Isolate_against_Isolate.txt', sep='\t', header=None)
os.makedirs('Data/contigs_coverage', exist_ok=True)
l = list(df[0].unique())
d = {k: f'contig_{(i+1):02d}' for i, k in enumerate(l) }
for f in l:
a = df.loc[df[0] == f]
a[0] = d[f]
a.to_csv(f'Data/contigs_coverage/{d[f]}_contigcov_dataframe.csv')
/tmp/ipykernel_787645/3375605565.py:10: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
a[0] = d[f]
The following inputs are required:
# === Parameters ===
gff_path = "Data/isolat_gff.xlsx"
orthogroups_path = "Data/Orthogroups_isolate.tsv"
fasta_path = "Data/formatted-Wolbachia-contigs.fa"
coverage_folder = "Data/contigs_coverage"
output_path = "Output/synteny_with_coverage.pdf"
output_path_png = "Figures/synteny_with_coverage.png"
# === Load data ===
df = pd.read_excel(gff_path)
orthogroups_df = pd.read_csv(orthogroups_path, sep="\t")
df.columns = [col.strip().lower() for col in df.columns]
df['protein_id'] = df['proteines'].str.extract(r'___(\d+)$')[0]
# === Annotation dictionary ===
protein_function_map = {}
for _, row in orthogroups_df.iterrows():
if pd.notna(row['PROTEINS']):
for pid in row['PROTEINS'].split(','):
protein_function_map[pid.strip()] = str(row['WOqueries_041124'])
# === Color assignment function ===
def get_color(annotations):
if annotations is None:
return 'grey'
annotations = annotations.split(",")
# Default: first annotation in the list (comma-separated)
annotation = annotations[0]
for a in annotations:
# Prioritize cif annotations over other functional labels
if "cif" in a:
annotation = a
a = annotation
return {
'HEAD': '#330357', 'ANK': '#d62727', 'CON_': '#1f38b4', 'TailFiber': '#f5d0de',
'Tail_': '#e36f82', 'RECOMB': '#07f256', 'LIGA': '#712ca3', 'TOXIN': '#de8209',
'WOPC1': '#4aeaed', 'cifA': '#e89edf', 'cifB': '#e89edf', 'TR': '#7340f5',
'PATATIN': '#593601', 'TNP': '#fcfc2d', 'MutL': '#272545', 'METHYL': '#538201',
'REP': '#88d4eb'
}.get(
next((k for k in [
'HEAD','ANK','CON_','TailFiber','Tail_','RECOMB','LIGA','TOXIN','WOPC1','cifA','cifB',
'TR','PATATIN','TNP','MutL','METHYL','REP'
] if k in a), None),
'#c7c5c5'
)
# === Contig lengths from FASTA ===
fasta_dico = {f'c_0000000000{(i+1):02d}': len(record.seq) for i, record in enumerate(SeqIO.parse(fasta_path, "fasta"))}
unique_contigs = sorted(df['contigs'].unique(), key=lambda x: fasta_dico.get(x, 0), reverse=True)
max_len = max(fasta_dico.values())
# === Prepare combined figure ===
fig = plt.figure(figsize=(15, len(unique_contigs) * 2))
y_step = 0.60 / len(unique_contigs)
y_height_syn = y_step * 0.6
y_height_cov = y_step * 0.3
y_margin = y_step * 0.05
for idx, contig in enumerate(unique_contigs):
subset = df[df['contigs'] == contig]
contig_len = fasta_dico.get(contig, subset['fin'].max())
width_ratio = contig_len / max_len
match = re.search(r'(\d+)$', contig)
display_name = f"Contig {int(match.group(1))}" if match else contig
y_base = 1 - (idx + 1) * y_step
# True contig length (from FASTA)
contig_length = fasta_dico.get(contig, subset['fin'].max()) # fallback if contig not found in FASTA
# --- Synteny ---
ax_syn = fig.add_axes([0.1, y_base + y_height_cov + y_margin, 0.8 * width_ratio, y_height_syn])
ax_syn.set_xlim(0, contig_len)
ax_syn.set_ylim(-1, 1)
ax_syn.axis('off')
ax_syn.text(-1000, 0, display_name, ha='right', va='center', fontsize=8)
ax_syn.hlines(0, 0, contig_len, color='black', linewidth=2, zorder=0)
# Display contig length on the right
ax_syn.text(-1000, -0.6, f"{contig_length:,} bp", va='center', ha='right', fontsize=8, color='black')
for _, row in subset.iterrows():
start, end = row['deb'], row['fin']
direction = row['sens']
prot_id = str(row['protein_id'])
annotation = protein_function_map.get(prot_id)
color = get_color(annotation)
arrow = (
FancyArrowPatch((start, 0), (end, 0), arrowstyle='simple', color=color,
mutation_scale=15, linewidth=1.5)
if direction == '+'
else FancyArrowPatch((end, 0), (start, 0), arrowstyle='simple', color=color,
mutation_scale=15, linewidth=1.5)
)
ax_syn.add_patch(arrow)
# --- Coverage ---
ax_cov = fig.add_axes([0.1, y_base, 0.8 * width_ratio, y_height_cov])
filename = f"contig_{int(match.group(1)):02d}_contigcov_dataframe.csv"
cov_path = os.path.join(coverage_folder, filename)
if os.path.exists(cov_path):
df_cov = pd.read_csv(cov_path, header=0, names=["contig", "position", "coverage"]).reset_index(drop=True)
df_cov = df_cov.dropna()
df_cov["position"] = pd.to_numeric(df_cov["position"], errors='coerce')
df_cov["coverage"] = pd.to_numeric(df_cov["coverage"], errors='coerce')
ax_cov.plot(df_cov["position"], df_cov["coverage"], color='blue', linewidth=0.7)
ax_cov.fill_between(df_cov["position"], 0, df_cov["coverage"], color='blue', alpha=0.2)
ax_cov.set_xlim(0, contig_len)
ax_cov.set_ylabel("Cov.", fontsize=6)
ax_cov.tick_params(axis='both', labelsize=6)
ax_cov.spines['top'].set_visible(False)
ax_cov.spines['right'].set_visible(False)
else:
ax_cov.axis('off')
ax_cov.text(0, 0.5, 'Coverage missing', fontsize=6)
# === Define legend elements ===
legend_elements = [
mpatches.Patch(color='#330357', label='Head'),
mpatches.Patch(color='#d62727', label='Ankyrin'),
mpatches.Patch(color='#1f38b4', label='Connector/Baseplate'),
mpatches.Patch(color='#f5d0de', label='Tail Fiber'),
mpatches.Patch(color='#e36f82', label='Tail'),
mpatches.Patch(color='#e89edf', label='CifA/B'),
mpatches.Patch(color='#07f256', label='Recombinase'),
mpatches.Patch(color='#fcfc2d', label='Transposase'),
mpatches.Patch(color='#4aeaed', label='WOPC1'),
mpatches.Patch(color='#de8209', label='Toxins'),
mpatches.Patch(color='#712ca3', label='LigA'),
mpatches.Patch(color='#7340f5', label='TR'),
mpatches.Patch(color='#593601', label='Patatin'),
mpatches.Patch(color='#272545', label='MutL'),
mpatches.Patch(color='#88d4eb', label='Replication/Repair'),
mpatches.Patch(color='#538201', label='Methylase')
]
# === Add legend on the right ===
fig.legend(handles=legend_elements, loc='center left', bbox_to_anchor=(0.91, 0.5),
title="Legend", fontsize=8, title_fontsize=9)
# === Export final figure ===
fig.savefig(output_path, bbox_inches='tight')
fig.savefig(output_path_png, bbox_inches='tight')
plt.close(fig)
print("Figure saved:", output_path)
Figure saved: Output/synteny_with_coverage.pdf
