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. */ {