533cc89eb4febc78c9f8f104aed4523de06bf85b max Fri Jun 19 08:58:02 2026 -0700 varFreqs: fix numeric filter rendering and trim per-cohort filters #Preview2 week - bugs introduced now will need a build patch to fix The Affected/Background track config pages showed none of the AF/AC/AN filters. hgTrackUi only discovers a numeric filter from a filter. (or Filter) setting (FILTER_NUMBER_WILDCARD in bigBedFilter.h); the generated fragment had only filterByRange./filterLimits. for those fields, so they were silently dropped. vcfToBigBed.py now emits filter. (a full-range default, so nothing is filtered by default) plus filterLimits for every numeric filter. That made ~140 filters appear, which overwhelmed the config page, so the per-database and per-population AF/AC filters are emitted commented out (range_filter enabled=False) and can be re-enabled individually by removing the leading "# ". Only the global filters remain active: variant type, consequence, the affected/case and background AF/AC/AN summaries, the affected/case-cohort and background-source selectors, the in-affected flag, and the length filters. The per-cohort data columns are unchanged in the bigBed and still show on the details page. No bigBed rebuild needed; this is trackDb-only. refs #36642 diff --git src/hg/makeDb/scripts/varFreqs/vcfToBigBed.py src/hg/makeDb/scripts/varFreqs/vcfToBigBed.py index 35c96b421bb..01bd3dd313f 100755 --- src/hg/makeDb/scripts/varFreqs/vcfToBigBed.py +++ src/hg/makeDb/scripts/varFreqs/vcfToBigBed.py @@ -714,80 +714,99 @@ f.write(" # Length filters\n") len_ranges = { "refLen": ("Reference Length", 1, len_stats["max_ref_len"]), "altLen": ("Alternate Length", 1, len_stats["max_alt_len"]), "varLen": ("Length Change", len_stats["min_var_len"], len_stats["max_var_len"]), } for fld, (label, lo, hi) in len_ranges.items(): f.write(f" filterByRange.{fld} on\n") f.write(f" filterLabel.{fld} {label}\n") f.write(f" filter.{fld} {lo}:{hi}\n") f.write(f" filterLimits.{fld} {lo}:{hi}\n") # Affected and background frequency summaries (both tracks carry both, # so e.g. the Affected track can be filtered to variants rare in the - # background). String range filters mirror the per-database AF/AC fields. + # background). + # + # IMPORTANT: hgTrackUi only DISCOVERS a numeric filter from a + # "filter." (or "Filter") setting -- see + # FILTER_NUMBER_WILDCARD in hg/inc/bigBedFilter.h and + # tdbGetTrackNumFilters() in hg/lib/hui.c. A field that has only + # filterByRange./filterLimits. is silently dropped. So every range + # filter needs filter. (the default selected range) to render; + # filterLimits. only sets the slider bounds shown in the UI. + # We set filter. to the FULL range so nothing is filtered by + # default. AC/AN are unbounded counts; the caps below are generous + # upper bounds (well above the true per-pool maxima) and never hide data. + AC_CAP_AFFECTED = 500000 # ~130k affected individuals -> AN up to ~260k + AC_CAP_BACKGROUND = 5000000 # ~1.5M individuals pooled -> AN up to ~3M + AC_CAP_DB = 2000000 # largest single cohort (FinnGen) AN ~1M + + def range_filter(field, label, lo, hi, enabled=True): + # enabled=False emits the same four settings commented out, so the + # filter is documented and trivially re-enabled by removing the "# ". + pre = " " if enabled else " # " + f.write(f"{pre}filterByRange.{field} on\n") + f.write(f"{pre}filterLabel.{field} {label}\n") + f.write(f"{pre}filter.{field} {lo}:{hi}\n") + f.write(f"{pre}filterLimits.{field} {lo}:{hi}\n") + f.write(" # Affected/case frequency summary\n") - f.write(" filterByRange.affectedAF on\n") - f.write(" filterLabel.affectedAF Affected/case AF (pooled)\n") - f.write(" filterLimits.affectedAF 0:1\n") - f.write(" filterByRange.affectedAC on\n") - f.write(" filterLabel.affectedAC Affected/case AC\n") - f.write(" filterByRange.affectedAN on\n") - f.write(" filterLabel.affectedAN Affected/case AN (pool denominator)\n") + range_filter("affectedAF", "Affected/case AF (pooled)", 0, 1) + range_filter("affectedAC", "Affected/case AC", 0, AC_CAP_AFFECTED) + range_filter("affectedAN", "Affected/case AN (pool denominator)", + 0, AC_CAP_AFFECTED) f.write(" # Background (population + unaffected) frequency summary\n") - f.write(" filterByRange.backgroundAF on\n") - f.write(" filterLabel.backgroundAF Background AF (pooled)\n") - f.write(" filterLimits.backgroundAF 0:1\n") - f.write(" filterByRange.backgroundAC on\n") - f.write(" filterLabel.backgroundAC Background AC (population + unaffected)\n") - f.write(" filterByRange.backgroundAN on\n") - f.write(" filterLabel.backgroundAN Background AN (pool denominator)\n") + range_filter("backgroundAF", "Background AF (pooled)", 0, 1) + range_filter("backgroundAC", "Background AC (population + unaffected)", + 0, AC_CAP_BACKGROUND) + range_filter("backgroundAN", "Background AN (pool denominator)", + 0, AC_CAP_BACKGROUND) f.write(" # Affected/case membership flag\n") - f.write(" filterByRange.inAffected on\n") - f.write(" filterLabel.inAffected Seen in an affected/case arm (1=yes, 0=no)\n") - f.write(" filter.inAffected 0:1\n") - f.write(" filterLimits.inAffected 0:1\n") - - # Per-database AF and AC filters - f.write(" # Per-database AF filters\n") + range_filter("inAffected", "Seen in an affected/case arm (1=yes, 0=no)", + 0, 1) + + # Per-database and per-population AF/AC filters. There are ~130 of these + # and they overwhelm the track config page, so they are emitted COMMENTED + # OUT (enabled=False). The data columns are still in the bigBed and show + # on the details page; only the filter controls are off. Re-enable any of + # them by deleting the leading "# " on its four lines. + f.write(" # Per-database AF filters (commented out: re-enable as needed)\n") for db_key, db_info in databases.items(): - f.write(f" filterByRange.{db_key}AF on\n") - f.write(f" filterLabel.{db_key}AF " - f"{db_info['name']} AF\n") + range_filter(f"{db_key}AF", f"{db_info['name']} AF", 0, 1, + enabled=False) - f.write(" # Per-database AC filters\n") + f.write(" # Per-database AC filters (commented out: re-enable as needed)\n") for db_key, db_info in databases.items(): - f.write(f" filterByRange.{db_key}AC on\n") - f.write(f" filterLabel.{db_key}AC " - f"{db_info['name']} AC\n") + range_filter(f"{db_key}AC", f"{db_info['name']} AC", 0, AC_CAP_DB, + enabled=False) - # Population-specific filters - f.write(" # Population-specific AF filters\n") + # Population-specific filters (commented out: re-enable as needed) + f.write(" # Population-specific AF/AC filters (commented out: re-enable as needed)\n") for db_key, db_info in databases.items(): if not db_info["pops"]: continue f.write(f" # {db_info['name']} populations\n") for pop in db_info["pops"]: - f.write(f" filterByRange.{db_key}AF_{pop['key']} on\n") - f.write(f" filterLabel.{db_key}AF_{pop['key']} " - f"{db_info['name']} {pop['name']} AF\n") + range_filter(f"{db_key}AF_{pop['key']}", + f"{db_info['name']} {pop['name']} AF", 0, 1, + enabled=False) for pop in db_info["pops"]: - f.write(f" filterByRange.{db_key}AC_{pop['key']} on\n") - f.write(f" filterLabel.{db_key}AC_{pop['key']} " - f"{db_info['name']} {pop['name']} AC\n") + range_filter(f"{db_key}AC_{pop['key']}", + f"{db_info['name']} {pop['name']} AC", 0, AC_CAP_DB, + enabled=False) f.write(" skipEmptyFields on\n") print(f"Wrote trackDb fragment: {output_path}", file=sys.stderr) # ============================================================================ # Main # ============================================================================ def main(): parser = argparse.ArgumentParser( description="Convert annotated VCF to bigBed (two-phase parallel)") parser.add_argument("--annotated-vcf", default="merged.annotated.vcf.gz") parser.add_argument("--output-prefix", default="varFreqs")