@@ -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()">↓ Download XLSX</button></summary>
1045+ <summary><h2>Per-genome classification</h2><button class="pdf-btn" onclick="event.stopPropagation();exportTableXlsx()">↓ 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\' )">↓ 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\' )">↓ 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