79764235e8868753201b1b5cd8278ff4f1dd1c08
max
Sat Jul 11 08:52:39 2026 -0700
Add nucleotide-position indicator to UTR and non-coding exon mouseovers
When zoomed to codon level, hovering a coding transcript's UTR now shows its
HGVS position, c.-N in the 5' UTR (counting back to the start codon) and c.*N
in the 3' UTR (counting forward from the stop). Non-coding transcripts, which
have no codon boxes, get the same treatment on their exons using HGVS n.
numbering from the transcript's 5' end. Distances are measured in spliced
mRNA space, so introns don't inflate them and the numbering runs continuously
across multi-exon UTRs. Previously these regions showed only strand, exon
number and length, with no indication of which nucleotides they covered.
refs #37866
diff --git src/hg/hgTracks/simpleTracks.c src/hg/hgTracks/simpleTracks.c
index 704ac6cd420..515b60ed6b0 100644
--- src/hg/hgTracks/simpleTracks.c
+++ src/hg/hgTracks/simpleTracks.c
@@ -2915,30 +2915,82 @@
linkedFeaturesMoveWinStart(xExonStart, bufferToEdge, newWinSize, &newWinStart, &newWinEnd);
if (!revStrand)
safef(mouseOverText, sizeof(mouseOverText), "%s (%d/%d)", prevExonText, numExons-exonIx, numExons);
else
safef(mouseOverText, sizeof(mouseOverText), "%s (%d/%d)", nextExonText, exonIx+1, numExons);
mapBoxJumpTo(hvg, x, y, w, h, tg, virtChromName, newWinStart, newWinEnd, mouseOverText);
result = TRUE;
break;
}
}
slFreeList(&exonList);
slFreeList(&crList);
return result;
}
+static int splicedBaseCount(struct linkedFeatures *lf, int gStart, int gEnd)
+/* Number of exonic (spliced mRNA) bases in the genomic half-open interval
+ * [gStart, gEnd), summed over the transcript's exon blocks (lf->components).
+ * Measuring in spliced space means introns don't inflate UTR distances. */
+{
+if (gStart >= gEnd)
+ return 0;
+int total = 0;
+struct simpleFeature *sf;
+for (sf = lf->components; sf != NULL; sf = sf->next)
+ {
+ int s = max(sf->start, gStart);
+ int e = min(sf->end, gEnd);
+ if (e > s)
+ total += e - s;
+ }
+return total;
+}
+
+static void utrHgvsCoord(struct linkedFeatures *lf, int g, char *buf, int bufSize)
+/* Format the HGVS CDS-relative coordinate for the single UTR base at genomic
+ * position g, without the leading "c." : "-N" in the 5' UTR (counting back to
+ * the first coding base) or "*N" in the 3' UTR (counting forward from the last
+ * coding base). Distances are spliced, and query orientation is taken from
+ * lf->orientation so the same code serves both strands. */
+{
+boolean posStrand = (lf->orientation >= 0);
+int cdsStart = lf->tallStart, cdsEnd = lf->tallEnd;
+if ((posStrand && g < cdsStart) || (!posStrand && g >= cdsEnd))
+ {
+ int n = posStrand ? splicedBaseCount(lf, g, cdsStart)
+ : splicedBaseCount(lf, cdsEnd, g + 1);
+ safef(buf, bufSize, "-%d", n);
+ }
+else
+ {
+ int n = posStrand ? splicedBaseCount(lf, cdsEnd, g + 1)
+ : 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);
+}
+
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
if (startsWith("chain", tg->tdb->type) || startsWith("lrg", tg->tdb->track))
@@ -3121,54 +3173,87 @@
else
{
aaToAbbr(aaLetter, aaAbbr, sizeof(aaAbbr));
aaName = aaToName(aaLetter);
}
dyStringPrintf(codonDy, "Codon: c.%d-%d (p.%d)
",
cStart, cEnd, pPos);
if (!isEmpty(aaAbbr))
{
if (aaName != NULL)
dyStringPrintf(codonDy, "Amino acid: %s - %s
", aaAbbr, aaName);
else
dyStringPrintf(codonDy, "Amino acid: %s
", aaAbbr);
}
}
+ else if (lf->tallStart < lf->tallEnd)
+ {
+ // UTR block of a coding transcript (codonIndex 0, so no
+ // c./p. above): label it with its HGVS UTR range. codonS/
+ // codonE span the whole UTR portion of this exon.
+ boolean posStrand = (lf->orientation >= 0);
+ int gFivePrime = posStrand ? codonS : codonE - 1;
+ int gThreePrime = posStrand ? codonE - 1 : codonS;
+ char loBuf[16], hiBuf[16];
+ utrHgvsCoord(lf, gFivePrime, loBuf, sizeof(loBuf));
+ utrHgvsCoord(lf, gThreePrime, hiBuf, sizeof(hiBuf));
+ char *utrSide = ((posStrand && codonS < lf->tallStart) ||
+ (!posStrand && codonS >= lf->tallEnd)) ? "5' UTR" : "3' UTR";
+ if (sameString(loBuf, hiBuf))
+ dyStringPrintf(codonDy, "%s: c.%s
", utrSide, loBuf);
+ else
+ dyStringPrintf(codonDy, "%s: c.%s_%s
", utrSide, loBuf, hiBuf);
+ }
// if you change the text below, also change hgTracks:mouseOverToExon
dyStringPrintf(codonDy, "Strand: %s
Exon: %s %d of %d Length: %d bp
%s",
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 *exonOrIntron = "Intron";
char *lengthLabel = "Length:";
if (isExon)
{
- posNote = "Codons: Zoom in to show cDNA position
";
exonOrIntron = "Exon";
lengthLabel = "Exon Length:";
+ if (lf->tallStart >= lf->tallEnd && zoomedToCdsColorLevel)
+ {
+ // non-coding transcript (no CDS): label the exon with its
+ // spliced HGVS n. nucleotide range instead of the 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), "Position: n.%d
", n5);
+ else
+ safef(posBuf, sizeof(posBuf), "Position: n.%d_%d
", n5, n3);
+ posNote = posBuf;
+ }
+ else
+ posNote = "Codons: Zoom in to show cDNA position
";
}
safef(mouseOverText, sizeof(mouseOverText), "Transcript: %s
%s"
"Strand: %s
%s: %s %d of %d %s %d bp
%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