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, "5' UTR: c.%s
", loBuf);
+ else
+ safef(buf, bufSize, "5' UTR: c.%s_%s
", 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, "Codons: c.%d-%d (p.%d)
", c5, c3, p5);
+ else
+ safef(buf + len, bufSize - len, "Codons: c.%d-%d (p.%d-%d)
",
+ 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, "3' UTR: c.%s
", loBuf);
+ else
+ safef(buf + len, bufSize - len, "3' UTR: c.%s_%s
", 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), "Position: n.%d
", n5);
else
- safef(posBuf, sizeof(posBuf), "Position: n.%d_%d
", n5, n3);
- posNote = posBuf;
+ safef(posBuf, sizeof(posBuf), "Position: n.%d_%d
",
+ n5, n3);
}
else
- posNote = "Codons: Zoom in to show cDNA position
";
+ // 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), "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