40b4fc89ec72c22cae51bf9a8b9a7e8dd887eebf chmalee Thu Jun 4 15:34:16 2026 -0700 Show MANE-relative HGVS terms on myVariants SNV detail pages, refs #33808 Co-Authored-By: Claude Opus 4.8 (1M context) diff --git src/hg/hgc/myVariantsClick.c src/hg/hgc/myVariantsClick.c index 6c3d2cb4789..2cfed4e85f9 100644 --- src/hg/hgc/myVariantsClick.c +++ src/hg/hgc/myVariantsClick.c @@ -1,494 +1,602 @@ /* Handle details pages for myVariants tracks */ /* Copyright (C) 2013 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "hash.h" #include "linefile.h" #include "hgc.h" #include "myVariants.h" #include "myVariantsShare.h" #include "obscure.h" #include "cheapcgi.h" #include "hgMaf.h" #include "hui.h" #include "hCommon.h" #include "wikiLink.h" #include "jsHelper.h" #include "web.h" #include "hgConfig.h" #include "jsonWrite.h" #include "htmshell.h" +#include "hgHgvs.h" +#include "variantProjector.h" +#include "genePred.h" +#include "genbank.h" +#include "bigBed.h" +#include "chromAlias.h" + +static struct genePred *getGeneTrackOverlaps(struct trackDb *geneTdb, char *db, char *chrom, + int start, int end) +/* Return transcripts in geneTdb overlapping chrom:start-end, from a bigGenePred file + * or a genePred SQL table. */ +{ +struct genePred *gpList = NULL; +if (sameString(geneTdb->type, "bigGenePred")) + { + char *fileName = hReplaceGbdb(trackDbSetting(geneTdb, "bigDataUrl")); + if (isEmpty(fileName)) + return NULL; + struct bbiFile *bbi = bigBedFileOpenAlias(fileName, chromAliasFindAliases); + struct lm *lm = lmInit(0); + struct bigBedInterval *bb, *bbList = bigBedIntervalQuery(bbi, chrom, start, end, 0, lm); + for (bb = bbList; bb != NULL; bb = bb->next) + { + struct genePred *gp = (struct genePred *)genePredFromBigGenePred(chrom, bb); + if (gp != NULL) + slAddHead(&gpList, gp); + } + lmCleanup(&lm); + bigBedFileClose(&bbi); + freeMem(fileName); + } +else + { + struct sqlConnection *conn = hAllocConn(db); + int rowOffset = 0; + struct sqlResult *sr = hRangeQuery(conn, geneTdb->table, chrom, start, end, NULL, &rowOffset); + char **row; + while ((row = sqlNextRow(sr)) != NULL) + slAddHead(&gpList, genePredLoad(row + rowOffset)); + sqlFreeResult(&sr); + hFreeConn(&conn); + } +slReverse(&gpList); +return gpList; +} + +static void printMyVariantsHgvs(struct myVariants *item) +/* For SNV items, show the genomic HGVS g. term and a c./n. term for each overlapping + * transcript in the assembly's default (MANE-first) gene track. */ +{ +if (!sameOk(item->itemType, "snv")) + return; +char *db = item->db; +struct seqWindow *gSeqWin = chromSeqWindowNew(db, NULL, 0, 0); +struct bed3 variantBed; +ZeroVar(&variantBed); +variantBed.chrom = item->chrom; +variantBed.chromStart = item->chromStart; +variantBed.chromEnd = item->chromEnd; +char *chromAcc = hRefSeqAccForChrom(db, item->chrom); +/* vpGenomicToTranscript requires an IUPAC alt (or "*"/"") and aborts otherwise, and + * rewrites "" in place; work on a copy, normalize "", and skip transcript terms + * for any alt it can't project. */ +char *alt = cloneString(item->alt); +boolean altProjectable = (isAllNt(alt, strlen(alt)) || + sameString(alt, "*") || sameString(alt, "")); +if (sameString(alt, "")) + alt[0] = '\0'; +char *hgvsG = hgvsGFromVariant(gSeqWin, &variantBed, alt, chromAcc, FALSE); + +struct trackDb *geneTdb = altProjectable ? hgvsDefaultGeneTrack(db) : NULL; +if (geneTdb != NULL) + htmlPrintf("HGVS (relative to %s):
\n", geneTdb->shortLabel); +else + printf("HGVS:
\n"); +if (isNotEmpty(hgvsG)) + htmlPrintf("  %s
\n", hgvsG); + +if (geneTdb == NULL) + return; + +struct genePred *gp, *gpList = getGeneTrackOverlaps(geneTdb, db, item->chrom, + item->chromStart, item->chromEnd); +for (gp = gpList; gp != NULL; gp = gp->next) + { + char *seq = txSeqFromGp(db, gp); + struct dnaSeq *txSeq = newDnaSeq(seq, strlen(seq), gp->name); + struct genbankCds cds; + genePredToCds(gp, &cds); + struct psl *psl = genePredToPsl(gp, hChromSize(db, gp->chrom), txSeq->size); + vpExpandIndelGaps(psl, gSeqWin, txSeq); + struct vpTx *vpTx = vpGenomicToTranscript(gSeqWin, &variantBed, alt, psl, txSeq); + char *hgvsTx = NULL; + if (cds.end > cds.start) + hgvsTx = hgvsCFromVpTx(vpTx, gSeqWin, psl, &cds, txSeq, FALSE); + else + hgvsTx = hgvsNFromVpTx(vpTx, gSeqWin, psl, txSeq, FALSE); + if (isNotEmpty(hgvsTx)) + { + if (isNotEmpty(gp->name2)) + htmlPrintf("  %s (%s):%s
\n", gp->name, gp->name2, hgvsTx); + else + htmlPrintf("  %s:%s
\n", gp->name, hgvsTx); + } + } +} void doMyVariantsDetails(struct customTrack *ct, char *itemIdString) /* Show details of a myVariants item. */ { jsIncludeFile("hgc.js",NULL); char *idString = cloneString(itemIdString); char *trackName = ct->tdb->track; /* Detect shared track and resolve table/permissions via hgcentral so that * revoked or downgraded shares no longer return owner data. */ boolean isShared = isMyVariantsSharedTrack(trackName); char *dataOwner = NULL; /* user whose table holds the data */ char *scopeProject = NULL; /* live share's project, or NULL for own track */ char *scopeDb = NULL; /* live share's db, or NULL for own track */ int permission = MYVAR_PERM_READONLY; if (isShared) { struct myVariantsShare *share = myVariantsResolveSharedTrack(trackName, cart); if (share == NULL) { printf("Share is no longer available.\n"); return; } /* Shared tracks are per-assembly; reject details requests from other dbs. */ if (!sameString(share->db, database)) { printf("This share is for a different assembly.\n"); myVariantsShareFree(&share); return; } dataOwner = cloneString(share->ownerUser); scopeProject = cloneString(share->project); scopeDb = cloneString(share->db); permission = share->permission; myVariantsShareFree(&share); } else dataOwner = cloneString(getUserName()); /* Anon users never edit shared items, even when the share is read-write. */ boolean canEdit = !isShared || (permission == MYVAR_PERM_READWRITE && getUserName() != NULL); char *tableName = myVariantsGetDbTable(dataOwner); /* idString is " ": parse id, query by id, verify name matches. */ char *idStrCopy = cloneString(idString); char *expectedName = strchr(idStrCopy, ' '); if (expectedName == NULL) { printf("Invalid item identifier.\n"); freeMem(idStrCopy); return; } *expectedName++ = '\0'; if (!isAllDigits(idStrCopy)) { printf("Invalid item identifier.\n"); freeMem(idStrCopy); return; } unsigned itemId = sqlUnsigned(idStrCopy); struct sqlConnection *conn = hAllocConn(CUSTOM_TRASH); struct dyString *query = sqlDyStringCreate( "select * from %s where id=%u", tableName, itemId); if (isNotEmpty(scopeDb)) sqlDyStringPrintf(query, " and db='%s'", scopeDb); if (isNotEmpty(scopeProject) && !sameString(scopeProject, "*")) sqlDyStringPrintf(query, " and project='%s'", scopeProject); struct sqlResult *sr = sqlGetResult(conn, query->string); dyStringFree(&query); struct myVariants *item = NULL; char **row = sqlNextRow(sr); if (row != NULL) { item = myVariantsLoad(row); if (!sameOk(item->name, expectedName)) myVariantsFree(&item); } sqlFreeResult(&sr); freeMem(idStrCopy); if (item != NULL) { /* Show shared banner */ if (isShared) { if (canEdit) printf("
" "Shared from %s
\n", htmlEncode(dataOwner)); else printf("
" "Shared from %s (read-only)
\n", htmlEncode(dataOwner)); } if (canEdit) { boolean isTranscript = sameOk(item->itemType, "transcript"); boolean isCnv = sameOk(item->itemType, "cnv"); webIncludeResourceFile("spectrum.min.css"); jsIncludeFile("spectrum.min.js", NULL); printf("
\n\n", hgTracksName()); cartSaveSession(cart); /* Save away ID string in hidden var. */ char varName[128]; char idStr[128]; safef(varName, sizeof(varName), "%s_%s", trackName, "id"); safef(idStr, sizeof(idStr), "%d", item->id); cgiMakeHiddenVar(varName, idStr); /* Put up editable label. */ safef(varName, sizeof(varName), "%s_%s", trackName, "name"); printf("Label: "); cgiMakeTextVar(varName, item->name, 17); printInfoIcon("A short label for this annotation, displayed in the browser."); printf("
\n"); /* Put up editable description. */ safef(varName, sizeof(varName), "%s_%s", trackName, "description"); printf("Description: "); printInfoIcon("Longer notes or comments about this annotation. Displayed on this details page."); printf("
\n"); cgiMakeTextArea(varName, item->description, 8, 80); printf("
\n"); /* Non-editable chromosome. */ htmlPrintf("Chromosome: %s
\n", item->chrom); /* Editable start and end. */ int chromSize = hChromSize(database, item->chrom); char chromSizeString[16]; safef(chromSizeString, sizeof(chromSizeString), "%d", chromSize); printf("Start: "); safef(varName, sizeof(varName), "%s_%s", trackName, "chromStart"); cgiMakeIntVarInRange(varName, item->chromStart, NULL, 80, "0", chromSizeString); printInfoIcon("0-based start position on the chromosome."); printf("
\n"); printf("End: "); safef(varName, sizeof(varName), "%s_%s", trackName, "chromEnd"); cgiMakeIntVarInRange(varName, item->chromEnd, NULL, 80, "1", chromSizeString); printInfoIcon("0-based half-open end position on the chromosome."); printf("
\n"); if (isTranscript) { /* Editable CDS Start / CDS End (stored as thickStart/thickEnd). */ printf("CDS Start: "); safef(varName, sizeof(varName), "%s_%s", trackName, "thickStart"); cgiMakeIntVarInRange(varName, item->thickStart, NULL, 80, "0", chromSizeString); printInfoIcon("Start of the coding region."); printf("
\n"); printf("CDS End: "); safef(varName, sizeof(varName), "%s_%s", trackName, "thickEnd"); cgiMakeIntVarInRange(varName, item->thickEnd, NULL, 80, "0", chromSizeString); printInfoIcon("End of the coding region."); printf("
\n"); /* Blocks (BED12). Hidden inputs are kept in sync by the widget; * updateBlocksFields in hgTracks reads them on submit. */ char vBC[128], vBS[128], vBT[128], vCS[128], vCE[128]; safef(vBC, sizeof(vBC), "%s_blockCount", trackName); safef(vBS, sizeof(vBS), "%s_blockSizes", trackName); safef(vBT, sizeof(vBT), "%s_chromStarts", trackName); safef(vCS, sizeof(vCS), "%s_chromStart", trackName); safef(vCE, sizeof(vCE), "%s_chromEnd", trackName); struct dyString *sizesCsv = dyStringNew(64); struct dyString *startsCsv = dyStringNew(64); int bi; for (bi = 0; bi < item->blockCount; bi++) { if (bi > 0) { dyStringAppendC(sizesCsv, ','); dyStringAppendC(startsCsv, ','); } dyStringPrintf(sizesCsv, "%d", item->blockSizes[bi]); dyStringPrintf(startsCsv, "%d", item->chromStarts[bi]); } char countStr[32]; safef(countStr, sizeof(countStr), "%u", item->blockCount); printf("Blocks: "); printInfoIcon("BED12-style blocks. Leave empty to store a single full-span block."); printf("
\n"); cgiMakeHiddenVarWithIdExtra(vBC, vBC, countStr, NULL); cgiMakeHiddenVarWithIdExtra(vBS, vBS, dyStringContents(sizesCsv), NULL); cgiMakeHiddenVarWithIdExtra(vBT, vBT, dyStringContents(startsCsv), NULL); printf("
\n"); jsIncludeFile("myVariantsBlocks.js", NULL); jsInlineF( "$(function(){\n" " if (typeof myVariantsBlocks === 'undefined') { return; }\n" " var sizesEl = document.getElementById('%s');\n" " var startsEl = document.getElementById('%s');\n" " var sizes = sizesEl.value ? sizesEl.value.split(',').map(Number) : [];\n" " var starts = startsEl.value ? startsEl.value.split(',').map(Number) : [];\n" " myVariantsBlocks.mount('myVariantsBlocksUi', {\n" " initialSizes: sizes,\n" " initialStarts: starts,\n" " getStart: function(){ return parseInt(document.getElementById('%s').value, 10); },\n" " getEnd: function(){ return parseInt(document.getElementById('%s').value, 10); },\n" " hiddenCountInput: document.getElementById('%s'),\n" " hiddenSizesInput: sizesEl,\n" " hiddenStartsInput: startsEl\n" " });\n" "});\n", vBS, vBT, vCS, vCE, vBC); dyStringFree(&sizesCsv); dyStringFree(&startsCsv); } /* Edit the color */ safef(varName, sizeof(varName), "%s_%s", trackName, "itemRgb"); char colorHex[8]; safef(colorHex, sizeof(colorHex), "#%06X", item->itemRgb); hPrintf(" ", varName); hPrintf("\n", varName, varName, colorHex); jsInlineF( "$(function() {" "$(document.getElementById('%s')).spectrum({" "preferredFormat: 'hex'," "showInput: true," "showPalette: true," "hideAfterPaletteSelect: true" "});" "});\n", varName); printf("
"); /* Edit ref/alt. Transcript has no alleles; CNV stores a sequence in alt. */ if (isCnv) { safef(varName, sizeof(varName), "%s_%s", trackName, "cnvType"); printf("CNV type: "); htmlPrintf(""); printInfoIcon("CNV vocabulary follows gnomAD"); printf("
\n"); safef(varName, sizeof(varName), "%s_%s", trackName, "alt"); printf("Sequence: "); cgiMakeTextVar(varName, item->alt, 40); printInfoIcon("Inserted or duplicated sequence at this position."); printf("
\n"); } else if (!isTranscript) { safef(varName, sizeof(varName), "%s_%s", trackName, "ref"); printf("Ref: "); cgiMakeTextVar(varName, item->ref, 17); printInfoIcon("Reference allele sequence at this position."); printf("
\n"); safef(varName, sizeof(varName), "%s_%s", trackName, "alt"); printf("Alt: "); cgiMakeTextVar(varName, item->alt, 17); printInfoIcon("Alternate (variant) allele sequence."); printf("
\n"); } /* Project: locked for shared tracks, editable for own track */ printf("Project: "); if (isShared) { htmlPrintf("%s
\n", isNotEmpty(item->project) ? item->project : "(none)"); } else { safef(varName, sizeof(varName), "%s_%s", trackName, "project"); struct slName *projects = myVariantsGetProjects(dataOwner); if (projects) { char selectName[128]; safef(selectName, sizeof(selectName), "%s_projectSelect", trackName); htmlPrintf(" "); htmlPrintf("", varName, varName, item->project); slFreeList(&projects); } else cgiMakeTextVar(varName, item->project, 40); printInfoIcon("Group annotations by project."); printf("
\n"); } /* Mouseover */ safef(varName, sizeof(varName), "%s_%s", trackName, "mouseover"); printf("Mouseover: "); cgiMakeTextVar(varName, item->mouseover, 60); printInfoIcon("Short text shown when hovering over this item."); printf("
\n"); /* Custom fields */ { struct slName *customCols = myVariantsGetCustomFields(dataOwner); if (customCols) { struct dyString *cfQuery = dyStringNew(256); struct slName *col; sqlDyStringPrintf(cfQuery, "SELECT "); boolean first = TRUE; for (col = customCols; col != NULL; col = col->next) { if (!first) sqlDyStringPrintf(cfQuery, ", "); sqlDyStringPrintIdList(cfQuery, col->name); first = FALSE; } sqlDyStringPrintf(cfQuery, " FROM %s WHERE id=%d", tableName, item->id); struct sqlResult *cfSr = sqlGetResult(conn, dyStringContents(cfQuery)); char **cfRow = sqlNextRow(cfSr); if (cfRow) { int i = 0; for (col = customCols; col != NULL; col = col->next, i++) { safef(varName, sizeof(varName), "%s_%s", trackName, col->name); printf("%s: ", col->name); cgiMakeTextVar(varName, cfRow[i] ? cfRow[i] : "", 40); printf("
\n"); } } sqlFreeResult(&cfSr); dyStringFree(&cfQuery); slFreeList(&customCols); } } /* Buttons: Update and Cancel always; Delete only for own track */ cgiMakeButton("submit", "Update"); printf(" "); if (!isShared) { safef(varName, sizeof(varName), "%s_%s", trackName, "delete"); cgiMakeButton(varName, "Delete"); printf(" "); } safef(varName, sizeof(varName), "%s_%s", trackName, "cancel"); cgiMakeButton(varName, "Cancel"); printf("
\n"); } else { /* Read-only display for shared items without write permission. * htmlPrintf escapes %s by default; that prevents stored XSS via * owner-controlled fields. */ htmlPrintf("Label: %s
\n", item->name); htmlPrintf("Description:
\n%s
\n", item->description); htmlPrintf("Chromosome: %s
\n", item->chrom); printf("Start: %d
\n", item->chromStart); printf("End: %d
\n", item->chromEnd); if (item->blockCount > 1) { printf("Blocks: "); int roBi; for (roBi = 0; roBi < item->blockCount; roBi++) { if (roBi > 0) printf(", "); int relStart = item->chromStarts[roBi]; int relEnd = relStart + item->blockSizes[roBi]; printf("%d-%d (%dbp)", relStart, relEnd, item->blockSizes[roBi]); } printf("
\n"); } char colorHex[8]; safef(colorHex, sizeof(colorHex), "#%06X", item->itemRgb); printf("Color:   %s
\n", colorHex, colorHex); boolean roIsCnv = sameOk(item->itemType, "cnv"); if (roIsCnv && isNotEmpty(item->cnvType)) htmlPrintf("CNV type: %s
\n", item->cnvType); if (isNotEmpty(item->ref)) htmlPrintf("Ref: %s
\n", item->ref); if (isNotEmpty(item->alt)) htmlPrintf("%s: %s
\n", roIsCnv ? "Sequence" : "Alt", item->alt); if (isNotEmpty(item->project)) htmlPrintf("Project: %s
\n", item->project); if (isNotEmpty(item->mouseover)) htmlPrintf("Mouseover: %s
\n", item->mouseover); /* Custom fields read-only */ { struct slName *customCols = myVariantsGetCustomFields(dataOwner); if (customCols) { struct dyString *cfQuery = dyStringNew(256); struct slName *col; sqlDyStringPrintf(cfQuery, "SELECT "); boolean first = TRUE; for (col = customCols; col != NULL; col = col->next) { if (!first) sqlDyStringPrintf(cfQuery, ", "); sqlDyStringPrintIdList(cfQuery, col->name); first = FALSE; } sqlDyStringPrintf(cfQuery, " FROM %s WHERE id=%d", tableName, item->id); struct sqlResult *cfSr = sqlGetResult(conn, dyStringContents(cfQuery)); char **cfRow = sqlNextRow(cfSr); if (cfRow) { int i = 0; for (col = customCols; col != NULL; col = col->next, i++) { if (cfRow[i] && cfRow[i][0]) htmlPrintf("%s: %s
\n", col->name, cfRow[i]); } } sqlFreeResult(&cfSr); dyStringFree(&cfQuery); slFreeList(&customCols); } } } printf("id: %d
\n", item->id); + printMyVariantsHgvs(item); + /* Overlaps section: only emit if hg.conf names overlap tracks for this assembly. * Format: myVariantsOverlapTracks. = track1,track2,... */ char overlapKey[256]; safef(overlapKey, sizeof(overlapKey), "myVariantsOverlapTracks.%s", database); char *overlapList = cfgOption(overlapKey); if (isNotEmpty(overlapList)) { struct jsonWrite *jw = jsonWriteNew(); jsonWriteListStart(jw, NULL); struct slName *trackNames = slNameListFromComma(overlapList); struct slName *t; for (t = trackNames; t != NULL; t = t->next) jsonWriteString(jw, NULL, t->name); jsonWriteListEnd(jw); jsInline("var doItemOverlaps = true;\n"); jsInlineF("var overlapTracks = %s;\n", jw->dy->string); printf("
\n"); slFreeList(&trackNames); jsonWriteFree(&jw); } printPosOnChrom(item->chrom, item->chromStart, item->chromEnd, NULL, TRUE, NULL); } freeMem(dataOwner); hFreeConn(&conn); }