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&nbsp;&nbsp;<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