2521d696f5073ce8cee3f59f423f161fdb77d550
max
  Fri Sep 25 02:48:37 2026 -0700
VCF tracks: the bigBed trackDb filters (filter.*, filterByRange, filterLimits, filterValues, filterType, filterText, filterLabel) now work on INFO fields, plus ID and QUAL. The field list and types come from the VCF header; the bigBed filter code is reused, bigBed behavior unchanged, refs #37617

diff --git src/hg/hgTracks/vcfTrack.c src/hg/hgTracks/vcfTrack.c
index 6dedc243815..fb164dd7e1d 100644
--- src/hg/hgTracks/vcfTrack.c
+++ src/hg/hgTracks/vcfTrack.c
@@ -2,30 +2,31 @@
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "bigWarn.h"
 #include "dystring.h"
 #include "rainbow.h"
 #include "errCatch.h"
 #include "fa.h"
 #include "genePredReader.h"
 #include "hacTree.h"
 #include "hdb.h"
 #include "hgColors.h"
 #include "hgTracks.h"
+#include "bigBedFilter.h"
 #include "iupac.h"
 #include "net.h"
 #include "pgSnp.h"
 #include "phyloTree.h"
 #include "trackHub.h"
 #include "trashDir.h"
 #include "variantProjector.h"
 #include "vcf.h"
 #include "vcfUi.h"
 #include "knetUdc.h"
 #include "udc.h"
 #include "memgfx.h"
 #include "chromAlias.h"
 #include "hgConfig.h"
 #include "wigCommon.h"
@@ -227,53 +228,217 @@
     nextRecord = rec->next;
     boolean allRef = TRUE;
     for (sample = sampleOrder; sample != NULL; sample = sample->next)
         {
         gt = vcfRecordFindGenotype(rec, sample->name);
         if (gt && !(gt->hapIxA == 0 && gt->hapIxB == 0) )
             allRef = FALSE;
         }
     if (!allRef)
         slAddHead(&retList, rec);
     }
 slReverse(&retList);
 vcff->records = retList;
 }
 
+struct vcfInfoFilter
+/* One generic trackDb filter (filter.*, filterText.*, filterValues.*, as for bigBed) on a VCF
+ * INFO field or on the fixed ID or QUAL column. */
+    {
+    struct vcfInfoFilter *next;
+    char *field;                    /* INFO key, or ID or QUAL */
+    const struct vcfInfoDef *def;   /* Header definition of the INFO key, NULL for ID and QUAL */
+    struct bigBedFilter *filter;    /* The comparison, made by the bigBed filter code */
+    };
+
+static void addVcfInfoFilter(struct vcfInfoFilter **pList, struct vcfFile *vcff, char *field,
+                             struct bigBedFilter *filter)
+/* If filter is not NULL, add it to *pList as a filter on the INFO field (or ID/QUAL column) field. */
+{
+if (filter == NULL)
+    return;
+struct vcfInfoFilter *vf;
+AllocVar(vf);
+vf->field = field;
+vf->def = vcfInfoDefForKey(vcff, field);
+vf->filter = filter;
+slAddHead(pList, vf);
+}
+
+static void checkVcfFilterField(struct vcfFile *vcff, struct trackDb *tdb, char *field)
+/* errAbort if a trackDb filter is on a field that is neither a declared INFO key nor ID/QUAL,
+ * like the bigBed filters do for a field that is not in the autoSql. */
+{
+if (vcfInfoDefForKey(vcff, field) == NULL && differentString(field, "ID")
+&& differentString(field, "QUAL"))
+    errAbort("track %s: trackDb filter on field '%s', but the VCF header has no INFO "
+             "definition for it.", tdb->track, field);
+}
+
+static struct vcfInfoFilter *vcfBuildInfoFilters(struct vcfFile *vcff, struct trackDb *tdb)
+/* Build the generic trackDb filters of a VCF track, using the bigBed filter code, where the
+ * field names are INFO keys. The field number of the bigBed filter is not used. */
+{
+struct vcfInfoFilter *list = NULL;
+struct trackDbFilter *tdbFilter;
+for (tdbFilter = tdbGetTrackNumFilters(tdb);  tdbFilter != NULL;  tdbFilter = tdbFilter->next)
+    {
+    checkVcfFilterField(vcff, tdb, tdbFilter->fieldName);
+    addVcfInfoFilter(&list, vcff, tdbFilter->fieldName,
+                     bigBedMakeNumberFilterOnField(cart, tdb, tdbFilter->name, NULL,
+                                                   tdbFilter->fieldName, 0, FALSE));
+    }
+for (tdbFilter = tdbGetTrackTextFilters(tdb);  tdbFilter != NULL;  tdbFilter = tdbFilter->next)
+    {
+    checkVcfFilterField(vcff, tdb, tdbFilter->fieldName);
+    addVcfInfoFilter(&list, vcff, tdbFilter->fieldName,
+                     bigBedMakeFilterTextOnField(cart, tdb, tdbFilter->name,
+                                                 tdbFilter->fieldName, 0, FALSE));
+    }
+filterBy_t *filterBy;
+for (filterBy = filterBySetGet(tdb, cart, NULL);  filterBy != NULL;  filterBy = filterBy->next)
+    {
+    checkVcfFilterField(vcff, tdb, filterBy->column);
+    if (filterBy->slChoices && differentString(filterBy->slChoices->name, "All"))
+        addVcfInfoFilter(&list, vcff, filterBy->column,
+                         bigBedMakeFilterByOnField(cart, tdb, filterBy->column, 0,
+                                                   filterBy->slChoices, FALSE));
+    }
+return list;
+}
+
+static char *vcfDatumToString(union vcfDatum datum, enum vcfInfoType type, char *buf, int bufSize)
+/* Return the string form of one INFO value, for the bigBed filter comparison. */
+{
+switch (type)
+    {
+    case vcfInfoInteger:
+        safef(buf, bufSize, "%d", datum.datInt);
+        return buf;
+    case vcfInfoFloat:
+        // 15 significant digits: the value parsed from the VCF text comes back unchanged
+        safef(buf, bufSize, "%.15g", datum.datFloat);
+        return buf;
+    case vcfInfoCharacter:
+        safef(buf, bufSize, "%c", datum.datChar);
+        return buf;
+    default:
+        return datum.datString;
+    }
+}
+
+static boolean vcfInfoFilterPass(struct vcfRecord *rec, struct vcfInfoFilter *vf)
+/* Return TRUE if rec passes one generic trackDb filter.
+ * - A field with several values (e.g. Number=A) passes if any of its values passes. For
+ *   filterType multipleListOr/multipleListAnd the values are compared as a list, like a
+ *   comma-separated bigBed field.
+ * - A missing value ('.' or key absent) fails a numeric filter (as in bcftools view -i). For text
+ *   and value filters it is compared as the empty string, so the wildcard '*' still shows it.
+ * - A Flag field has the value 1 if the key is present and 0 if it is absent. */
+{
+struct bigBedFilter *filter = vf->filter;
+boolean isNumeric = (filter->comparisonType == COMPARE_LESS
+                     || filter->comparisonType == COMPARE_MORE
+                     || filter->comparisonType == COMPARE_BETWEEN);
+boolean isList = (filter->comparisonType == COMPARE_HASH_LIST_AND
+                  || filter->comparisonType == COMPARE_HASH_LIST_OR);
+if (vf->def == NULL)
+    {
+    // the fixed columns ID or QUAL
+    char *val = sameString(vf->field, "ID") ? rec->name : rec->qual;
+    if (sameString(val, "."))
+        return isNumeric ? FALSE : bigBedFilterOneValue(filter, "");
+    return bigBedFilterOneValue(filter, val);
+    }
+const struct vcfInfoElement *el = vcfRecordFindInfo(rec, vf->field);
+if (vf->def->type == vcfInfoFlag)
+    {
+    char *val = "0";
+    if (el != NULL)
+        {
+        val = "1";
+        // older VCFs can have a value on a flag, e.g. VCF 3.2's DB=0 or DB=1
+        if (el->count > 0 && isNotEmpty(el->values[0].datString))
+            val = el->values[0].datString;
+        }
+    return bigBedFilterOneValue(filter, val);
+    }
+struct dyString *listVal = isList ? dyStringNew(0) : NULL;
+boolean gotValue = FALSE;
+int i;
+for (i = 0;  el != NULL && i < el->count;  i++)
+    {
+    if (el->missingData[i])
+        continue;
+    char buf[64];
+    char *val = vcfDatumToString(el->values[i], vf->def->type, buf, sizeof buf);
+    gotValue = TRUE;
+    if (isList)
+        {
+        if (dyStringLen(listVal) > 0)
+            dyStringAppendC(listVal, ',');
+        dyStringAppend(listVal, val);
+        }
+    else if (bigBedFilterOneValue(filter, val))
+        return TRUE;
+    }
+if (isList)
+    {
+    boolean pass = bigBedFilterOneValue(filter, dyStringContents(listVal));
+    dyStringFree(&listVal);
+    return pass;
+    }
+if (!gotValue && !isNumeric)
+    return bigBedFilterOneValue(filter, "");
+return FALSE;
+}
+
+static boolean vcfInfoFiltersFail(struct vcfRecord *rec, struct vcfInfoFilter *filters)
+/* Return TRUE if rec fails any of the generic trackDb filters. */
+{
+struct vcfInfoFilter *vf;
+for (vf = filters;  vf != NULL;  vf = vf->next)
+    if (!vcfInfoFilterPass(rec, vf))
+        return TRUE;
+return FALSE;
+}
+
 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);
+struct vcfInfoFilter *infoFilters = vcfBuildInfoFilters(vcff, tdb);
 int filtOut = 0;
-if (gotQualFilter || gotFilterFilter || gotMinFreqFilter || gotMinAcFilter)
+if (gotQualFilter || gotFilterFilter || gotMinFreqFilter || gotMinAcFilter || infoFilters != NULL)
     {
     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)) ||
-               (gotMinAcFilter && minAcFail(rec, minAc)) ))
+               (gotMinAcFilter && minAcFail(rec, minAc)) ||
+               (infoFilters != NULL && vcfInfoFiltersFail(rec, infoFilters)) ))
             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. */
 {