Skip to content

Commit e2a39a8

Browse files
committed
.
1 parent a232847 commit e2a39a8

1 file changed

Lines changed: 27 additions & 14 deletions

File tree

recmpox/recmpox.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,10 @@ def _snp_positions_svg(positions: List[int], genome_length: int, width_units: in
485485
# kbp scale marks (downward from baseline)
486486
kbp_pos = 0
487487
while kbp_pos <= genome_length:
488+
# Skip regular ticks too close to the endpoint (would produce duplicate label)
489+
if kbp_pos > 0 and (genome_length - kbp_pos) < step_bp * 0.6:
490+
kbp_pos += step_bp
491+
continue
488492
x = (kbp_pos / genome_length) * width_units
489493
label = "0" if kbp_pos == 0 else f"{kbp_pos // 1000}k"
490494
parts.append(
@@ -506,7 +510,7 @@ def _snp_positions_svg(positions: List[int], genome_length: int, width_units: in
506510
)
507511
parts.append(
508512
f'<text x="{x_end}" y="{y_kbp_label}" font-size="9" fill="#888"'
509-
f' text-anchor="end">{genome_length // 1000}k</text>'
513+
f' text-anchor="end">{genome_length:,}</text>'
510514
)
511515

512516
# SNP ticks (downward from baseline)
@@ -564,11 +568,15 @@ def _genome_ruler_html(genome_length: int, min_width: int) -> str:
564568
while pos <= genome_length:
565569
pct = pos / genome_length * 100
566570
label = "0" if pos == 0 else f"{pos // 1000}k"
571+
# Skip regular ticks that would crowd the endpoint label (within 60 % of one step)
572+
if pos > 0 and (genome_length - pos) < step_bp * 0.6:
573+
pos += step_bp
574+
continue
567575
ticks.append(f'<span class="ruler-tick" style="left:{pct:.2f}%">{label}</span>')
568576
pos += step_bp
569-
# Always include a tick at the genome end if not already there
577+
# Always include a tick at the genome end if not already there, showing exact bp
570578
if genome_length % step_bp != 0:
571-
ticks.append(f'<span class="ruler-tick" style="left:100%">{genome_length // 1000}k</span>')
579+
ticks.append(f'<span class="ruler-tick" style="left:100%">{genome_length:,}</span>')
572580
return f'<div class="strip-ruler" style="min-width:{min_width}px">{"".join(ticks)}</div>'
573581

574582

@@ -712,6 +720,11 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
712720

713721
# Diagnostic sites per sample: strip (genome position, color Ia/Ib/other) + table for ALL consensus genomes
714722
genome_length = results[0]["length"] if results else 0
723+
# Trim strip display to the last diagnostic SNP position (any allegiance, incl. other)
724+
if diagnostic_snp_positions and genome_length:
725+
display_length = max(diagnostic_snp_positions)
726+
else:
727+
display_length = genome_length
715728
_first_alle = next((r.get("allegiances", []) for r in results if r.get("allegiances")), [])
716729
strip_min_w = max(600, len(_first_alle) * 2)
717730
rec_sites_html = ""
@@ -726,10 +739,10 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
726739
for (pos, allegiance) in sorted_alle:
727740
cls = "ia" if allegiance == "ia" else ("ib" if allegiance == "ib" else "other")
728741
lbl = ref1_label if allegiance == "ia" else (ref2_label if allegiance == "ib" else "other")
729-
pct = pos / genome_length * 100 if genome_length else 0
742+
pct = pos / display_length * 100 if display_length else 0
730743
strip_segments += f'<span class="strip-segment {cls}" title="{pos} bp – {html_escape(lbl)}" style="left:{pct:.3f}%"></span>'
731744
section_cls = "rec-sites-section" + (" recombinant" if rec_call == "potential recombinant" else "")
732-
ruler_html = _genome_ruler_html(genome_length, strip_min_w)
745+
ruler_html = _genome_ruler_html(display_length, strip_min_w)
733746
rec_sites_html += (
734747
f'<div class="{section_cls}" data-row="{ri}" data-recombinant="{html_escape(rec_call)}">'
735748
f'<div class="rec-sites-row">'
@@ -804,13 +817,13 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
804817
merged_tracts[-1] = (merged_tracts[-1][0], end_pos, clade, merged_tracts[-1][3] + n_snps)
805818
else:
806819
merged_tracts.append((start_pos, end_pos, clade, n_snps))
807-
bp_strip_min_w = max(600, genome_length // 150) if genome_length else 600
820+
bp_strip_min_w = max(600, display_length // 150) if display_length else 600
808821
strip_segments = ""
809822
for j, (start_pos, end_pos, clade, n_snps) in enumerate(merged_tracts):
810823
cls = "ia" if clade == "ia" else "ib"
811824
lbl = ref1_label if clade == "ia" else ref2_label
812-
left_pct = start_pos / genome_length * 100 if genome_length else 0
813-
width_pct = max(0.3, (end_pos - start_pos + 1) / genome_length * 100) if genome_length else 2
825+
left_pct = start_pos / display_length * 100 if display_length else 0
826+
width_pct = max(0.3, (end_pos - start_pos + 1) / display_length * 100) if display_length else 2
814827
strip_segments += (
815828
f'<span class="strip-segment region-segment {cls}" title="{start_pos}{end_pos} {html_escape(lbl)} ({n_snps} SNPs)" style="left:{left_pct:.3f}%; width:{width_pct:.3f}%;"></span>'
816829
)
@@ -827,7 +840,7 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
827840
summary_text = "No recombination tracts (genome entirely one clade)"
828841
details_content = '<p class="threshold-note">No recombination detected; genome is entirely one clade.</p>'
829842
else:
830-
bp_ruler_html = _genome_ruler_html(genome_length, bp_strip_min_w)
843+
bp_ruler_html = _genome_ruler_html(display_length, bp_strip_min_w)
831844
strip_display = (
832845
f'<div class="strip-genome breakpoints-strip" style="min-width:{bp_strip_min_w}px" role="img" aria-label="Predicted regions and breakpoints">{strip_segments}</div>'
833846
+ bp_ruler_html
@@ -873,7 +886,7 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
873886
diagnostic_snp_positions, genome_length, num_bins=60
874887
)
875888
snp_histogram_json = json.dumps({"labels": hist_labels, "counts": hist_counts})
876-
ruler_svg = _snp_positions_svg(diagnostic_snp_positions, genome_length)
889+
ruler_svg = _snp_positions_svg(diagnostic_snp_positions, display_length)
877890
snp_positions_table_rows = "".join(
878891
f'<tr><td class="num">{i}</td><td class="num">{pos}</td></tr>'
879892
for i, pos in enumerate(diagnostic_snp_positions, start=1)
@@ -889,14 +902,14 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
889902
'<p class="threshold-note"><strong>{n_snps} diagnostic SNPs.</strong> Density of diagnostic SNPs along the reference (alignment coordinates). Use this to interpret where recombination breakpoints may fall. Squirrel always builds alignments relative to reference NC_003310 (Clade I) or NC_063383 (Clade II), not the refs you specified.</p>'
890903
'<div class="snp-positions-wrapper"><div class="chart-container" style="height:220px;"><canvas id="chartSnpPositions"></canvas></div></div>'
891904
'<h3 class="snp-ruler-title">Exact positions</h3>'
892-
'<p class="threshold-note">Each tick marks one diagnostic SNP position along the genome (0 to {genome_length} bp).</p>'
905+
'<p class="threshold-note">Each tick marks one diagnostic SNP position along the genome (0 to {display_length} bp).</p>'
893906
'<div class="snp-positions-wrapper snp-ruler-wrapper">' + ruler_svg + '</div>'
894907
'<details class="rec-sites-details"><summary>Show diagnostic site table</summary>'
895908
'<table class="rec-sites-table"><thead><tr><th>#</th><th>Position (bp)</th></tr></thead><tbody>'
896909
+ snp_positions_table_rows +
897910
'</tbody></table></details>'
898911
'</div></details>'
899-
).format(n_snps=n_snps, genome_length=genome_length)
912+
).format(n_snps=n_snps, genome_length=genome_length, display_length=display_length)
900913

901914
html = f"""<!DOCTYPE html>
902915
<html lang="en">
@@ -1029,7 +1042,7 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
10291042
{threshold_html}
10301043
</div>
10311044
<details class="collapsible-section" open>
1032-
<summary><h2>Per-genome classification (recombinant genomes)</h2><button class="pdf-btn" onclick="event.stopPropagation();exportTableXlsx()">&#8595; Download XLSX</button></summary>
1045+
<summary><h2>Per-genome classification</h2><button class="pdf-btn" onclick="event.stopPropagation();exportTableXlsx()">&#8595; Download XLSX</button></summary>
10331046
<div class="section-inner table-section">
10341047
<table id="t">
10351048
<thead>
@@ -1043,7 +1056,7 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
10431056
</div>
10441057
</details>
10451058
<details class="collapsible-section" open>
1046-
<summary><h2>Diagnostic SNPs per genome (stacked barplot)</h2><button class="pdf-btn" onclick="event.stopPropagation();exportChartPng(\'chartBar\',\'diagnostic_snps_barplot.png\')">&#8595; Download PNG</button></summary>
1059+
<summary><h2>Diagnostic SNPs per genome</h2><button class="pdf-btn" onclick="event.stopPropagation();exportChartPng(\'chartBar\',\'diagnostic_snps_barplot.png\')">&#8595; Download PNG</button></summary>
10471060
<div class="section-inner chart-section">
10481061
<p class="threshold-note">Stacked percentage per genome: % {html_escape(ref1_label)} (blue), % {html_escape(ref2_label)} (purple), % other (gray).</p>
10491062
<div class="chart-legend stacked-bar-legend">

0 commit comments

Comments
 (0)