c3322bc2d3f94a59721989762c58db1734ec7740 max Mon Sep 7 20:04:43 2026 -0700 hgTracks: show the cDNA and codon range of an exon in its mouseover instead of "Zoom in to show cDNA position" At gene-level zoom the exon popup only said "Codons: Zoom in to show cDNA position", so the only way to find a c. or p. position was to zoom into one exon after another. The popup now gives the exon's HGVS c. range and the codons it spans, e.g. "Codons: c.1364-1482 (p.455-494)", plus the c.-N / c.*N range of any UTR part of the exon. The numbers agree with the per-codon popups shown when zoomed in. Non-coding transcripts already got an n. range but only at codon-level zoom, where the number is of little use; that gate is gone, so they are labelled at every zoom too. Chain and LRG tracks have no cDNA coordinates and are left alone. refs #38278 diff --git src/hg/hgTracks/simpleTracks.c src/hg/hgTracks/simpleTracks.c index 9f0c3cd4b8d..2d0fcbfe828 100644 --- src/hg/hgTracks/simpleTracks.c +++ src/hg/hgTracks/simpleTracks.c @@ -2975,46 +2975,109 @@ : splicedBaseCount(lf, g, cdsStart); safef(buf, bufSize, "*%d", n); } } static int txMrnaPos(struct linkedFeatures *lf, int g) /* 1-based spliced (mRNA) position of genomic base g measured from the * transcript's 5' end. Used for HGVS n. numbering of non-coding transcripts. */ { if (lf->orientation >= 0) return splicedBaseCount(lf, lf->start, g + 1); else return splicedBaseCount(lf, g, lf->end); } +static int cdsMrnaPos(struct linkedFeatures *lf, int g) +/* 1-based HGVS c. position of genomic base g, which must lie in the CDS. + * c.1 is the first base of the CDS, distances measured in spliced space. */ +{ +if (lf->orientation >= 0) + return splicedBaseCount(lf, lf->tallStart, g) + 1; +else + return splicedBaseCount(lf, g + 1, lf->tallEnd) + 1; +} + +static void exonCdsNote(struct linkedFeatures *lf, int s, int e, char *buf, int bufSize) +/* Describe the exon [s,e) of a coding transcript in HGVS c. coordinates: the UTR + * piece(s) as c.-N / c.*N and the coding piece as a c. range together with the + * codon (p.) numbers it covers. This is what the popup shows when we are zoomed + * out too far to label the individual codons. */ +{ +buf[0] = '\0'; +boolean posStrand = (lf->orientation >= 0); +int cdsStart = lf->tallStart, cdsEnd = lf->tallEnd; +/* the exon split into its three possible pieces, in genomic coordinates */ +int upS = s, upE = min(e, cdsStart); // left of the CDS +int cdS = max(s, cdsStart), cdE = min(e, cdsEnd); +int dnS = max(s, cdsEnd), dnE = e; // right of the CDS +/* in transcription order the left piece is the 5' UTR on + strand, the 3' on - */ +int utr5S = posStrand ? upS : dnS, utr5E = posStrand ? upE : dnE; +int utr3S = posStrand ? dnS : upS, utr3E = posStrand ? dnE : upE; +char loBuf[16], hiBuf[16]; +int len = 0; +if (utr5E > utr5S) + { + utrHgvsCoord(lf, posStrand ? utr5S : utr5E - 1, loBuf, sizeof(loBuf)); + utrHgvsCoord(lf, posStrand ? utr5E - 1 : utr5S, hiBuf, sizeof(hiBuf)); + if (sameString(loBuf, hiBuf)) + safef(buf, bufSize, "<b>5' UTR: </b> c.%s<br>", loBuf); + else + safef(buf, bufSize, "<b>5' UTR: </b> c.%s_%s<br>", loBuf, hiBuf); + len = strlen(buf); + } +if (cdE > cdS) + { + int c5 = cdsMrnaPos(lf, posStrand ? cdS : cdE - 1); + int c3 = cdsMrnaPos(lf, posStrand ? cdE - 1 : cdS); + int p5 = (c5 + 2) / 3, p3 = (c3 + 2) / 3; + if (p5 == p3) + safef(buf + len, bufSize - len, "<b>Codons: </b> c.%d-%d (p.%d)<br>", c5, c3, p5); + else + safef(buf + len, bufSize - len, "<b>Codons: </b> c.%d-%d (p.%d-%d)<br>", + c5, c3, p5, p3); + len = strlen(buf); + } +if (utr3E > utr3S) + { + utrHgvsCoord(lf, posStrand ? utr3S : utr3E - 1, loBuf, sizeof(loBuf)); + utrHgvsCoord(lf, posStrand ? utr3E - 1 : utr3S, hiBuf, sizeof(hiBuf)); + if (sameString(loBuf, hiBuf)) + safef(buf + len, bufSize - len, "<b>3' UTR: </b> c.%s<br>", loBuf); + else + safef(buf + len, bufSize - len, "<b>3' UTR: </b> c.%s_%s<br>", loBuf, hiBuf); + } +} + void linkedFeaturesItemExonMaps(struct track *tg, struct hvGfx *hvg, void *item, double scale, int y, int heightPer, int sItem, int eItem, boolean lButton, boolean rButton, int buttonW) /* Draw mapBoxes over exons and introns labeled with exon/intron numbers */ { struct linkedFeatures *lf = item; struct simpleFeature *exons = lf->components; struct simpleFeature *exon = exons; char *exonText, *intronText; int numExons = 0; int exonIx = 1; struct slRef *exonList = NULL, *ref; // TODO this exonText (and intronText) setting is just a made-up placeholder. // could add a real setting name. Maybe someday extend to exon names (LRG?) instead of just exon numbers +boolean isTranscript = TRUE; // chain blocks and LRG regions have no cDNA coordinates if (startsWith("chain", tg->tdb->type) || startsWith("lrg", tg->tdb->track)) { + isTranscript = FALSE; exonText = trackDbSettingClosestToHomeOrDefault(tg->tdb, "exonText" , "Block"); intronText = trackDbSettingClosestToHomeOrDefault(tg->tdb, "intronText", "Gap" ); // what really goes here for chain type? } else { exonText = trackDbSettingClosestToHomeOrDefault(tg->tdb, "exonText" , "Exon" ); intronText = trackDbSettingClosestToHomeOrDefault(tg->tdb, "intronText", "Intron"); } while (exon != NULL) /* Make a stupid list of exons separate from what's given. */ /* It seems like lf->components isn't necessarily sorted. */ { refAdd(&exonList, exon); exon = exon->next; } @@ -3216,52 +3279,59 @@ strandStr, exonIntronText, exonIntronNumber, numExonIntrons, e - s, phaseText); tg->mapItem(tg, hvg, item, codonDy->string, tg->mapItemName(tg, item), sItem, eItem, codonsx, y, w, heightPer); // and restore the mouseOver lf->mouseOver = oldMouseOver; } } } } } else // either an intron, or else an exon zoomed out too far for codons (or no codons) { // if you change this text, make sure you also change hgTracks.js:mouseOverToLabel // if you change the text below, also change hgTracks:mouseOverToExon char *posNote = ""; - char posBuf[64]; + char posBuf[256]; char *exonOrIntron = "Intron"; char *lengthLabel = "Length:"; if (isExon) { exonOrIntron = "Exon"; lengthLabel = "Exon Length:"; - if (lf->tallStart >= lf->tallEnd && zoomedToCdsColorLevel) + if (isTranscript) + { + if (lf->tallStart >= lf->tallEnd) { // non-coding transcript (no CDS): label the exon with its - // spliced HGVS n. nucleotide range instead of the codon note. + // spliced HGVS n. nucleotide range instead of a codon note. boolean posStrand = (lf->orientation >= 0); int n5 = txMrnaPos(lf, posStrand ? s : e - 1); int n3 = txMrnaPos(lf, posStrand ? e - 1 : s); if (n5 == n3) safef(posBuf, sizeof(posBuf), "<b>Position: </b> n.%d<br>", n5); else - safef(posBuf, sizeof(posBuf), "<b>Position: </b> n.%d_%d<br>", n5, n3); - posNote = posBuf; + safef(posBuf, sizeof(posBuf), "<b>Position: </b> n.%d_%d<br>", + n5, n3); } else - posNote = "<b>Codons:</b> Zoom in to show cDNA position<br>"; + // coding transcript, too far out to draw the codons: give the + // exon's cDNA range and the codons it covers, so a c. or p. + // position can be found without zooming into every exon + exonCdsNote(lf, s, e, posBuf, sizeof(posBuf)); + posNote = posBuf; + } } safef(mouseOverText, sizeof(mouseOverText), "<b>Transcript:</b> %s<br>%s" "<b>Strand:</b> %s<br><b>%s:</b> %s %d of %d <b>%s</b> %d bp<br>%s", existingText, posNote, strandStr, exonOrIntron, exonIntronText, exonIntronNumber, numExonIntrons, lengthLabel, e - s, phaseText); // temporarily remove the mouseOver from the lf, since linkedFeatureMapItem will always // prefer a lf->mouseOver over the itemName char *oldMouseOver = lf->mouseOver; lf->mouseOver = NULL; tg->mapItem(tg, hvg, item, mouseOverText, tg->mapItemName(tg, item), sItem, eItem, sx, y, w, heightPer); // and restore the old mouseOver