be36584718d57384b01657483422f51adf29f0bb
max
  Sat Jul 25 18:22:02 2026 -0700
vcfTrack: add minimum alternate allele count (minAc) filter, refs #37927

When a VCF INFO column includes AC, offer a 'minimum allele count' filter in the
track UI (e.g. 2 to hide singletons) and drop records whose largest alternate
allele count is below the threshold.

diff --git src/hg/hgTracks/vcfTrack.c src/hg/hgTracks/vcfTrack.c
index 4186ccae7fe..844600a64c0 100644
--- src/hg/hgTracks/vcfTrack.c
+++ src/hg/hgTracks/vcfTrack.c
@@ -80,30 +80,43 @@
 }
 
 static boolean getMinFreq(struct trackDb *tdb, double *retMinFreq)
 /* Return TRUE and set retMinFreq if cart contains nonzero minimum minor allele frequency. */
 {
 double minFreq = cartOrTdbDouble(cart, tdb, VCF_MIN_ALLELE_FREQ_VAR, VCF_DEFAULT_MIN_ALLELE_FREQ);
 if (minFreq > 0)
     {
     if (retMinFreq != NULL)
 	*retMinFreq = minFreq;
     return TRUE;
     }
 return FALSE;
 }
 
+static boolean getMinAc(struct trackDb *tdb, int *retMinAc)
+/* Return TRUE and set retMinAc if cart contains a positive minimum allele count. */
+{
+int minAc = cartOrTdbInt(cart, tdb, VCF_MIN_AC_VAR, VCF_DEFAULT_MIN_AC);
+if (minAc > 0)
+    {
+    if (retMinAc != NULL)
+	*retMinAc = minAc;
+    return TRUE;
+    }
+return FALSE;
+}
+
 static boolean minFreqFail(struct vcfRecord *record, double minFreq)
 /* Return TRUE if record's INFO include AF (alternate allele frequencies) or AC+AN
  * (alternate allele counts and total count of observed alleles) and the minor allele
  * frequency < minFreq -- or rather, major allele frequency > (1 - minFreq) because
  * variants with > 2 alleles might have some significant minor frequencies along with
  * tiny minor frequencies). */
 {
 struct vcfFile *vcff = record->file;
 boolean gotInfo = FALSE;
 double refFreq = 1.0;
 double maxAltFreq = 0.0;
 int i;
 const struct vcfInfoElement *afEl = vcfRecordFindInfo(record, "AF");
 const struct vcfInfoDef *afDef = vcfInfoDefForKey(vcff, "AF");
 if (afEl != NULL && afDef != NULL && afDef->type == vcfInfoFloat)
@@ -162,30 +175,56 @@
                 maxAltFreq = atof(lastWordInLine(data));
                 refFreq -= maxAltFreq;
                 }
             }
         }
     }
 if (gotInfo)
     {
     double majorAlFreq = max(refFreq, maxAltFreq);
     if (majorAlFreq > (1.0 - minFreq))
 	return TRUE;
     }
 return FALSE;
 }
 
+static boolean minAcFail(struct vcfRecord *record, int minAc)
+/* Return TRUE if record's INFO includes AC (alternate allele counts) and the largest
+ * alternate allele count is less than minAc.  Records whose INFO lacks AC are not filtered. */
+{
+struct vcfFile *vcff = record->file;
+const struct vcfInfoElement *acEl = vcfRecordFindInfo(record, "AC");
+const struct vcfInfoDef *acDef = vcfInfoDefForKey(vcff, "AC");
+if (acEl == NULL || acDef == NULL || acDef->type != vcfInfoInteger)
+    return FALSE;
+int i;
+int maxAc = 0;
+boolean gotAc = FALSE;
+for (i = 0;  i < acEl->count;  i++)
+    {
+    if (acEl->missingData[i])
+	continue;
+    gotAc = TRUE;
+    int altCount = acEl->values[i].datInt;
+    if (altCount > maxAc)
+	maxAc = altCount;
+    }
+if (gotAc && maxAc < minAc)
+    return TRUE;
+return FALSE;
+}
+
 static void filterRefOnlyAlleles(struct vcfFile *vcff, struct trackDb *tdb)
 /* Drop items from VCF that don't differ from the reference for any of the
  * samples specified in trackDb */
 {
 struct vcfRecord *rec, *nextRecord, *retList = NULL;
 const struct vcfGenotype *gt;
 
 boolean hideOtherSamples = cartUsualBooleanClosestToHome(cart, tdb, FALSE, VCF_PHASED_HIDE_OTHER_VAR, FALSE);
 struct slPair *sample, *sampleOrder = vcfPhasedGetSampleOrder(cart, tdb, FALSE, hideOtherSamples);
 for (rec = vcff->records; rec != NULL; rec = nextRecord)
     {
     nextRecord = rec->next;
     boolean allRef = TRUE;
     for (sample = sampleOrder; sample != NULL; sample = sample->next)
         {
@@ -195,43 +234,46 @@
         }
     if (!allRef)
         slAddHead(&retList, rec);
     }
 slReverse(&retList);
 vcff->records = retList;
 }
 
 static void filterRecords(struct vcfFile *vcff, struct track *tg)
 /* If a filter is specified in the cart, remove any records that don't pass filter. Adapt longLabel if something was filtered. */
 {
 struct trackDb *tdb = tg->tdb;
 double minQual = VCF_DEFAULT_MIN_QUAL;
 struct slName *filterValues = NULL;
 double minFreq = VCF_DEFAULT_MIN_ALLELE_FREQ;
+int minAc = VCF_DEFAULT_MIN_AC;
 boolean gotQualFilter = getMinQual(tdb, &minQual);
 boolean gotFilterFilter = getFilterValues(tdb, &filterValues);
 boolean gotMinFreqFilter = getMinFreq(tdb, &minFreq);
+boolean gotMinAcFilter = getMinAc(tdb, &minAc);
 int filtOut = 0;
-if (gotQualFilter || gotFilterFilter || gotMinFreqFilter) 
+if (gotQualFilter || gotFilterFilter || gotMinFreqFilter || gotMinAcFilter)
     {
     struct vcfRecord *rec, *nextRec, *newList = NULL;
     for (rec = vcff->records;  rec != NULL;  rec = nextRec)
         {
         nextRec = rec->next;
         if (! ((gotQualFilter && minQualFail(rec, minQual)) ||
                (gotFilterFilter && filterColumnFail(rec, filterValues)) ||
-               (gotMinFreqFilter && minFreqFail(rec, minFreq)) ))
+               (gotMinFreqFilter && minFreqFail(rec, minFreq)) ||
+               (gotMinAcFilter && minAcFail(rec, minAc)) ))
             slAddHead(&newList, rec);
         else 
             filtOut++;
         }
     slReverse(&newList);
     vcff->records = newList;
     }
 
 labelTrackAsFilteredNumber(tg, filtOut);
 }
 
 struct pgSnpVcfStartEnd
 /* This extends struct pgSnp by tacking on an original VCF chromStart and End at the end,
  * for use by indelTweakMapItem below.  This can be cast to pgs. */
 {