@@ -1049,7 +1049,7 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
10491049
10501050 # Build list of dicts per row for JS (exclude allegiances to keep JSON small)
10511051 # Flatten tuple keys (merged display columns) and always include chart keys
1052- _chart_keys = {"id" , "pct_ia" , "pct_ib" , "pct_other" }
1052+ _chart_keys = {"id" , "pct_ia" , "pct_ib" , "pct_other" , "n_ia" , "n_ib" , "n_other" , "n_other_n" }
10531053 _data_keys = {k for c in cols for k in (c [0 ] if isinstance (c [0 ], tuple ) else (c [0 ],))} | _chart_keys
10541054 data_list = [{k : r .get (k , "" ) for k in _data_keys } for r in results ]
10551055 data_json = json .dumps (data_list ).replace ("</" , "<\\ /" )
@@ -1073,7 +1073,15 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
10731073 sorted_alle = sorted (allegiances , key = lambda x : x [0 ])
10741074 strip_segments = ""
10751075 for (pos , allegiance ) in sorted_alle :
1076- cls = "ia" if allegiance == "ia" else ("ib" if allegiance == "ib" else "other" )
1076+ # Use "other-n" for N's so the strip filter can show only N segments when "Other (N's)" is selected
1077+ if allegiance == "ia" :
1078+ cls = "ia"
1079+ elif allegiance == "ib" :
1080+ cls = "ib"
1081+ elif allegiance == "other_n" :
1082+ cls = "other other-n"
1083+ else :
1084+ cls = "other"
10771085 lbl = ref1_label if allegiance == "ia" else (ref2_label if allegiance == "ib" else "other" )
10781086 pct = pos / display_length * 100 if display_length else 0
10791087 strip_segments += f'<span class="strip-segment { cls } " title="{ pos } bp – { html_escape (lbl )} " style="left:{ pct :.3f} %"></span>'
@@ -1089,16 +1097,20 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
10891097 f'<p class="threshold-note">Tracts = consecutive diagnostic sites with same classification. One row per tract.</p>'
10901098 f'<table class="rec-sites-table"><thead><tr><th>Start (bp)</th><th>End (bp)</th><th>Clade</th><th>Sites</th></tr></thead><tbody>'
10911099 )
1092- # Group consecutive positions with same allegiance into tracts
1100+ # Group consecutive positions with same allegiance into tracts (treat other_n and ambiguous as "other")
1101+ def _eff_allegiance (a : str ) -> str :
1102+ return "other" if a in ("ambiguous" , "other_n" ) else a
1103+
10931104 i = 0
10941105 while i < len (sorted_alle ):
10951106 start_pos , allegiance = sorted_alle [i ]
10961107 end_pos = start_pos
1108+ eff = _eff_allegiance (allegiance )
10971109 j = i + 1
1098- while j < len (sorted_alle ) and sorted_alle [j ][1 ] == allegiance :
1110+ while j < len (sorted_alle ) and _eff_allegiance ( sorted_alle [j ][1 ]) == eff :
10991111 end_pos = sorted_alle [j ][0 ]
11001112 j += 1
1101- label = ref1_label if allegiance == "ia" else (ref2_label if allegiance == "ib" else "other" )
1113+ label = ref1_label if eff == "ia" else (ref2_label if eff == "ib" else "other" )
11021114 n_sites = j - i
11031115 rec_sites_html += f'<tr><td class="num">{ start_pos } </td><td class="num">{ end_pos } </td><td>{ html_escape (label )} </td><td class="num">{ n_sites } </td></tr>'
11041116 i = j
@@ -1112,6 +1124,15 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
11121124 '<details class="collapsible-section diagnostic-strips-chart" open id="diagnosticStripsSection">'
11131125 f'<summary><h2>Classification of diagnostic sites per sample</h2><button class="pdf-btn" onclick="event.stopPropagation();exportStripSvg(\' diagnosticStripsSection\' ,\' diagnosticStripsContainer\' ,\' Classification of diagnostic sites per sample\' ,\' { html_escape (ref1_label )} \' ,\' { html_escape (ref2_label )} \' )">↓ Download SVG</button></summary>'
11141126 '<div class="section-inner chart-section">'
1127+ '<div class="strip-classification-filter-wrap">'
1128+ '<label class="strip-filter-check-label"><input type="checkbox" id="stripClassificationFilterCheck" aria-describedby="stripClassificationOptions"> Filter strips by classification</label>'
1129+ '<div id="stripClassificationOptions" class="strip-classification-options" aria-hidden="true">'
1130+ '<label><input type="checkbox" class="strip-class-opt" data-filter="ia"> {ref1}</label>'
1131+ '<label><input type="checkbox" class="strip-class-opt" data-filter="ib"> {ref2}</label>'
1132+ '<label><input type="checkbox" class="strip-class-opt" data-filter="other_all"> Other (all)</label>'
1133+ '<label><input type="checkbox" class="strip-class-opt" data-filter="other_n"> Other (N\' s)</label>'
1134+ '</div>'
1135+ '</div>'
11151136 '<p class="threshold-note">One strip per consensus: each segment = one diagnostic site in genomic order. <span id="stripFilterCount" aria-live="polite"></span></p>'
11161137 '<p class="threshold-note">{ref1} (blue), {ref2} (orange), other (gray).</p>'
11171138 '<div class="strip-legend"><span class="strip-legend-ia"></span> {ref1} <span class="strip-legend-ib"></span> {ref2} <span class="strip-legend-other"></span> other</div>'
@@ -1331,6 +1352,8 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
13311352.strip-segment.ia {{ background: #4A90D9; }}
13321353.strip-segment.ib {{ background: #E89B3C; }}
13331354.strip-segment.other {{ background: #95a5a6; }}
1355+ .strip-segment.other-n {{ background: #95a5a6; }}
1356+ .strip-segment.segment-hidden-by-filter {{ display: none; }}
13341357.strip-segment.region-segment {{ min-width: 4px; border-radius: 3px; }}
13351358.strip-segment.breakpoint-marker {{ width: 4px; background: #c0392b; transform: translateX(-50%); }}
13361359#breakpointsStripsContainer .rec-sites-section {{ background: linear-gradient(135deg, #fafbfc 0%, #f4f6f9 100%); border-left: 4px solid #dee2e6; box-shadow: 0 1px 4px rgba(0,0,0,0.05); transition: box-shadow 0.15s; }}
@@ -1341,6 +1364,12 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
13411364.strip-legend-ib {{ display: inline-block; width: 14px; height: 14px; background: #E89B3C; border-radius: 2px; }}
13421365.strip-legend-other {{ display: inline-block; width: 14px; height: 14px; background: #95a5a6; border-radius: 2px; }}
13431366.strip-legend-gap {{ display: inline-block; width: 14px; height: 14px; background: linear-gradient(135deg, #c4d3e0, #b6c8d7); border: 1px solid #9fb8cc; border-radius: 2px; }}
1367+ .strip-classification-filter-wrap {{ display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 12px 20px; margin-bottom: 12px; padding: 10px 12px; background: #f8f9fa; border-radius: 6px; border: 1px solid #e9ecef; }}
1368+ .strip-classification-filter-wrap label {{ font-size: 0.95em; color: #495057; cursor: pointer; white-space: nowrap; }}
1369+ .strip-classification-filter-wrap input[type="checkbox"] {{ margin-right: 6px; vertical-align: middle; }}
1370+ .strip-classification-options {{ display: none; gap: 12px 20px; flex-wrap: wrap; align-items: center; }}
1371+ .strip-classification-options[aria-hidden="false"] {{ display: flex; }}
1372+ .strip-filter-check-label {{ font-weight: 600; }}
13441373.strip-ruler {{ position: relative; width: 100%; height: 22px; min-width: 200px; margin-top: 3px; }}
13451374.ruler-tick {{ position: absolute; transform: translateX(-50%); font-size: 0.67em; font-weight: 500; color: #6c757d; white-space: nowrap; line-height: 1; padding-top: 6px; letter-spacing: 0.01em; }}
13461375.ruler-tick::before {{ content: ''; display: block; position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 1px; height: 5px; background: #adb5bd; }}
@@ -1622,36 +1651,93 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
16221651 if (ri !== null) visibleRows.add(ri);
16231652 }}
16241653 }});
1654+ var stripFilterCheck = document.getElementById("stripClassificationFilterCheck");
1655+ var stripOpts = document.querySelectorAll(".strip-class-opt:checked");
1656+ // When "Filter strips by classification" is checked: figure is empty until you pick Ia, Ib, etc.
1657+ // When unchecked: show everything.
1658+ var stripClassificationOn = stripFilterCheck && stripFilterCheck.checked;
1659+ function rowMatchesStripClassification(ri) {{
1660+ if (!stripClassificationOn || !data[ri]) return true;
1661+ var r = data[ri];
1662+ var n_ia = (r.n_ia != null) ? (parseInt(r.n_ia, 10) || 0) : 0;
1663+ var n_ib = (r.n_ib != null) ? (parseInt(r.n_ib, 10) || 0) : 0;
1664+ var n_other = (r.n_other != null) ? (parseInt(r.n_other, 10) || 0) : 0;
1665+ var n_other_n = (r.n_other_n != null) ? (parseInt(r.n_other_n, 10) || 0) : 0;
1666+ var hasOtherAll = n_other > 0;
1667+ var hasOtherN = n_other_n > 0;
1668+ for (var i = 0; i < stripOpts.length; i++) {{
1669+ var f = stripOpts[i].getAttribute("data-filter");
1670+ if (f === "ia" && n_ia > 0) return true;
1671+ if (f === "ib" && n_ib > 0) return true;
1672+ if (f === "other_all" && hasOtherAll) return true;
1673+ if (f === "other_n" && hasOtherN) return true;
1674+ }}
1675+ return false;
1676+ }}
1677+ var stripVisibleRows = new Set();
1678+ visibleRows.forEach(function(ri) {{
1679+ if (rowMatchesStripClassification(ri)) stripVisibleRows.add(ri);
1680+ }});
1681+ // When filter is on but no option selected, show nothing; when options selected, show matching rows
1682+ var rowsForStrips = !stripClassificationOn ? visibleRows : (stripOpts.length > 0 ? stripVisibleRows : new Set());
16251683 var container = document.getElementById("diagnosticStripsContainer");
16261684 if (container) {{
16271685 var sections = container.querySelectorAll(".rec-sites-section");
1686+ var showIa = false, showIb = false, showOtherAll = false, showOtherN = false;
1687+ for (var i = 0; i < stripOpts.length; i++) {{
1688+ var f = stripOpts[i].getAttribute("data-filter");
1689+ if (f === "ia") showIa = true;
1690+ if (f === "ib") showIb = true;
1691+ if (f === "other_all") showOtherAll = true;
1692+ if (f === "other_n") showOtherN = true;
1693+ }}
16281694 sections.forEach(function(sec) {{
16291695 var ri = sec.getAttribute("data-row");
1630- sec.classList.toggle("hidden", !visibleRows.has(ri));
1696+ var sectionVisible = rowsForStrips.has(ri);
1697+ sec.classList.toggle("hidden", !sectionVisible);
1698+ var stripGenome = sec.querySelector(".strip-genome:not(.breakpoints-strip)");
1699+ if (stripGenome) {{
1700+ stripGenome.querySelectorAll(".strip-segment").forEach(function(seg) {{
1701+ if (!stripClassificationOn) {{
1702+ seg.classList.remove("segment-hidden-by-filter");
1703+ }} else if (sectionVisible) {{
1704+ var hide = false;
1705+ if (seg.classList.contains("ia")) hide = !showIa;
1706+ else if (seg.classList.contains("ib")) hide = !showIb;
1707+ else if (seg.classList.contains("other-n")) hide = !showOtherAll && !showOtherN;
1708+ else if (seg.classList.contains("other")) hide = !showOtherAll;
1709+ seg.classList.toggle("segment-hidden-by-filter", hide);
1710+ }}
1711+ }});
1712+ }}
16311713 }});
16321714 }}
16331715 var bpContainer = document.getElementById("breakpointsStripsContainer");
16341716 if (bpContainer) {{
16351717 var bpSections = bpContainer.querySelectorAll(".rec-sites-section");
16361718 bpSections.forEach(function(sec) {{
16371719 var ri = sec.getAttribute("data-row");
1638- sec.classList.toggle("hidden", !visibleRows .has(ri));
1720+ sec.classList.toggle("hidden", !rowsForStrips .has(ri));
16391721 }});
16401722 }}
16411723 var countEl = document.getElementById("stripFilterCount");
16421724 if (countEl) {{
1643- var n = visibleRows .size;
1725+ var n = rowsForStrips .size;
16441726 var total = rows.length;
1645- if (n < total && total > 0)
1727+ if (stripClassificationOn)
1728+ countEl.textContent = "Showing " + n + " of " + total + " samples (filtered by classification).";
1729+ else if (n < total && total > 0)
16461730 countEl.textContent = "Showing " + n + " of " + total + " samples.";
16471731 else
16481732 countEl.textContent = "";
16491733 }}
16501734 var bpCountEl = document.getElementById("breakpointsFilterCount");
16511735 if (bpCountEl) {{
1652- var n = visibleRows .size;
1736+ var n = rowsForStrips .size;
16531737 var total = rows.length;
1654- if (n < total && total > 0)
1738+ if (stripClassificationOn)
1739+ bpCountEl.textContent = "Showing " + n + " of " + total + " samples (filtered by classification).";
1740+ else if (n < total && total > 0)
16551741 bpCountEl.textContent = "Showing " + n + " of " + total + " samples.";
16561742 else
16571743 bpCountEl.textContent = "";
@@ -1668,6 +1754,25 @@ def _ref_box(label: str, spec: Optional[str]) -> str:
16681754 recFilterEl.addEventListener("change", applyFilters);
16691755 recFilterEl.addEventListener("input", applyFilters);
16701756 }}
1757+ var stripFilterCheckEl = document.getElementById("stripClassificationFilterCheck");
1758+ if (stripFilterCheckEl) {{
1759+ stripFilterCheckEl.addEventListener("change", function() {{
1760+ var opts = document.getElementById("stripClassificationOptions");
1761+ if (opts) opts.setAttribute("aria-hidden", this.checked ? "false" : "true");
1762+ applyFilters();
1763+ }});
1764+ }}
1765+ document.querySelectorAll(".strip-class-opt").forEach(function(el) {{
1766+ el.addEventListener("change", function() {{
1767+ // If any option is turned on, auto-open + check the master filter to make it obvious
1768+ var anyChecked = document.querySelector(".strip-class-opt:checked") != null;
1769+ var master = document.getElementById("stripClassificationFilterCheck");
1770+ var opts = document.getElementById("stripClassificationOptions");
1771+ if (master) master.checked = anyChecked;
1772+ if (opts) opts.setAttribute("aria-hidden", anyChecked ? "false" : "true");
1773+ applyFilters();
1774+ }});
1775+ }});
16711776
16721777 thead.querySelectorAll("th.sortable").forEach(function(th) {{
16731778 var colIndex = parseInt(th.dataset.col, 10);
@@ -2149,13 +2254,13 @@ def find_ref_key(ref_key: str, keys: List[str]) -> Optional[str]:
21492254 if not allegiances :
21502255 logger .warning ("Query %s: no diagnostic calls" , query_id )
21512256 continue
2152- n_ia , n_ib , n_other = allegiance_summary (allegiances )
2257+ n_ia , n_ib , n_other , n_other_n = allegiance_summary (allegiances )
21532258 total = n_ia + n_ib + n_other
21542259 pct_ia = round (100.0 * n_ia / total , 2 ) if total else 0
21552260 pct_ib = round (100.0 * n_ib / total , 2 ) if total else 0
21562261 pct_other = round (100.0 * n_other / total , 2 ) if total else 0
21572262 # SNP-only summary for consensus and deletion present (SNP-based interpretation)
2158- n_ia_snp , n_ib_snp , n_other_snp = allegiance_summary_snp_only (allegiances , diagnostic_snp_positions )
2263+ n_ia_snp , n_ib_snp , n_other_snp , _ = allegiance_summary_snp_only (allegiances , diagnostic_snp_positions )
21592264 total_snp = n_ia_snp + n_ib_snp + n_other_snp
21602265 consensus_snp = consensus_from_snp_percentages (
21612266 n_ia_snp , n_ib_snp , n_other_snp , ref1_label , ref2_label , pct_threshold = 10.0
@@ -2170,6 +2275,7 @@ def find_ref_key(ref_key: str, keys: List[str]) -> Optional[str]:
21702275 "n_ia" : n_ia ,
21712276 "n_ib" : n_ib ,
21722277 "n_other" : n_other ,
2278+ "n_other_n" : n_other_n ,
21732279 "pct_ia" : pct_ia ,
21742280 "pct_ib" : pct_ib ,
21752281 "pct_other" : pct_other ,
0 commit comments