6d437c28a5ff82a2af59ca516559053c4bf7c3f9 max Thu Jun 18 01:18:34 2026 -0700 hgTracks: amino-acid name in codon + ruler mouseovers; exon-length label; ruler "Complement bases" config toggle Three genome browser display changes around AA display: 1) amino acids shown on genePred codons and on rule codons, and making the "complement option" easier to find. 1) The genePred/bigGenePred codon mouseover (zoomed to the codon level) now shows the codon's amino acid on its own "Amino acid:" line, as the three-letter abbreviation plus full name (e.g. "Ala (alanine)"). Rather than reverse-decode the amino acid out of the codon's packed grayIx, the one-letter code is now stored on the codon when it is translated: struct simpleFeature gains a codonAa field, codonToGrayIx() reports the letter through a new out-param, and the codon mouseover reads simpleFeature.codonAa directly. The drawn codon letter and the mouseover then derive from the same translation and cannot drift apart, without the mouseover having to understand the grayIx encoding (the drawing path, colorAndCodonFromGrayIx(), still decodes its own grayIx inline, unchanged). Adds a one-letter->full-name aaToName() in lib/dnautil.c (using the previously unused name field of aminoAcidTable). Stop codons show "Ter (termination)". Also relabels the exon "Length" field as "Exon Length" in the codon and zoomed-out exon mouseovers; introns keep "Length". 2) The base-position ruler's three-reading-frame translation (hgt.baseShowCodons) now gives each codon box a mouse-over with the same three-letter abbreviation and full name, reading the stored codonAa via aaToName()/aaToAbbr() (baseColorDrawRulerCodons in cds.c). 3) Adds a "Complement the bases" checkbox to the base position (ruler) track configuration page (hgTrackUi rulerUi). It toggles the same per-assembly COMPLEMENT_BASES_VAR cart variable that was previously reachable only by clicking the "Click to complement" arrow next to the ruler. Also fixes the indentation of the adjacent drawComplementArrow() text assignments. refs #37779 Co-Authored-By: Claude Opus 4.8 (1M context) diff --git src/hg/hgTracks/cds.c src/hg/hgTracks/cds.c index e4e9b7e0f27..4a3dceb7a37 100644 --- src/hg/hgTracks/cds.c +++ src/hg/hgTracks/cds.c @@ -640,37 +640,43 @@ return peptide; } static int peptideToGrayIx(char peptide, boolean codonFirstColor) /* Encode peptide (a letter or '*') and alternating gray shade into our alpha-offset scheme. */ { if (peptide == '*') peptide = GRAYIX_STOP_CODON_ALPHA; if (codonFirstColor) return(peptide - 'A' + 1); else return(peptide - 'A' + 1 + 26); } static int codonToGrayIx(DNA *dna, bool codonFirstColor, boolean *foundStart, - boolean reverse, boolean colorStopStart) -/* Return grayIx encoding the codon and color (or alternating shades). */ + boolean reverse, boolean colorStopStart, char *retAa) +/* Return grayIx encoding the codon and color (or alternating shades). + * If retAa is non-NULL, also report the codon's display amino-acid letter + * (the AA letter, or '*' stop / 'M' start / 'X' error). retAa is the codon's + * semantic payload, computed here at translation time so consumers (e.g. the + * codon mouseover) need not reverse-decode it out of the packed grayIx. */ { if (reverse) reverseComplement(dna,strlen(dna)); char codonChar = baseColorLookupCodon(dna); +if (retAa != NULL) + *retAa = codonChar; if (codonChar == 'M' && foundStart != NULL && !(*foundStart)) *foundStart = TRUE; #ifdef LOWELAB if(sameString(dna,"GTG")) { return -'V'; } if(sameString(dna,"TTG")) { return -'L'; } #endif if (codonChar == '*') @@ -1259,31 +1265,31 @@ codon[offset] = *start++; if (offset != 2) continue; /* new codon */ AllocVar(sf); sf->start = chromPos - 3; sf->end = sf->start + 3; if (reverse) { sf->start = winEnd - sf->start + winStart - 3; sf->end = sf->start + 3; } // Base offsets mod 6 for alternating colors: 0,1,2 --> first codon, 3,4,5 --> second codon. bool codonFirstColor = (sf->start % 6 < 3); - sf->grayIx = codonToGrayIx(codon, codonFirstColor, NULL, FALSE, TRUE); + sf->grayIx = codonToGrayIx(codon, codonFirstColor, NULL, FALSE, TRUE, &sf->codonAa); zeroBytes(codon, 4); slAddHead(&sfList, sf); } slReverse(&sfList); return sfList; } struct simpleFeature *baseColorCodonsFromGenePred(struct linkedFeatures *lf, struct genePred *gp, boolean colorStopStart, boolean codonNumbering) /* Given an lf and the genePred from which the lf was constructed, * return a list of simpleFeature elements, one per codon (or partial * codon if the codon falls on a gap boundary. */ { unsigned *starts = gp->exonStarts; unsigned *ends = gp->exonEnds; @@ -1440,77 +1446,85 @@ * with indels around the one base. This code is fragile, so * just work around it by truncating the sequence. */ char tempCodonSeq[8]; if(posStrand) safef(tempCodonSeq, sizeof(tempCodonSeq), "%s%s", partialCodonSeq, theRestOfCodon); else safef(tempCodonSeq, sizeof(tempCodonSeq), "%s%s", theRestOfCodon, partialCodonSeq ); tempCodonSeq[4] = '\0'; // no more than 3 bases AllocVar(sf); sf->start = currentStart; sf->end = currentEnd; + sf->codonAa = 'X'; // stays 'X' for the out-of-CDS (error) branch below sf->grayIx = ((posStrand && currentEnd <= cdsEnd) || (!posStrand && currentStart >= cdsStart)) ? codonToGrayIx(tempCodonSeq, altColor, &foundStart, - !posStrand, colorStopStart) : + !posStrand, colorStopStart, &sf->codonAa) : GRAYIX_CDS_ERROR; sf->codonIndex = codonIndex; slAddHead(&sfList, sf); } break; } // end if we've gone off the end of the current exon currentSize = currentEnd - currentStart; /*inside a coding block (with 3 bases)*/ if (currentSize == 3) { AllocVar(sf); sf->start = currentStart; sf->end = currentEnd; if ((posStrand && currentEnd <= cdsEnd) || (!posStrand && currentStart >= cdsStart)) { char currentCodon[4]; char *thisDna = getCachedDna(currentStart, currentEnd); memcpy(currentCodon, thisDna, 3); currentCodon[3] = '\0'; sf->grayIx = codonToGrayIx(currentCodon, altColor, &foundStart, - !posStrand, colorStopStart); + !posStrand, colorStopStart, &sf->codonAa); // is this block less than 3 bases away from the previous block (ribo slip) + // (slip only changes the color; the amino-acid letter is unchanged) if (posStrand && (lastEnd + 3 > currentEnd)) sf->grayIx = - 'A' + 1 + 52 + baseColorLookupCodon(currentCodon); } else + { sf->grayIx = GRAYIX_CDS_ERROR; + sf->codonAa = 'X'; + } } /*start of a coding block with less than 3 bases*/ else if (currentSize < 3) { updatePartialCodon(partialCodonSeq, currentStart, currentEnd, posStrand); AllocVar(sf); sf->start = currentStart; sf->end = currentEnd; if (strlen(partialCodonSeq) == 3) sf->grayIx = codonToGrayIx(partialCodonSeq, altColor, - &foundStart, !posStrand, colorStopStart); + &foundStart, !posStrand, colorStopStart, &sf->codonAa); else + { sf->grayIx = GRAYIX_CDS_ERROR; + sf->codonAa = 'X'; + } strcpy(partialCodonSeq,"" ); /*update frame based on bases appended*/ frame -= currentSize; } else errAbort("%s: Too much dna (%d - %d = %d)
\n", lf->name, currentEnd, currentStart, currentSize); sf->codonIndex = codonIndex++; slAddHead(&sfList, sf); if(posStrand) currentStart = currentEnd; else currentEnd = currentStart; @@ -1651,31 +1665,31 @@ { if (cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE)) complement(dyMrnaSeq->string, dyMrnaSeq->stringSize); drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, dyMrnaSeq->string, zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE); } else if (drawOpt == baseColorDrawItemCodons) { if (e <= lf->tallEnd) { boolean startColor = FALSE; /* re-set color of this block based on mrna codons rather than * genomic, but keep the odd/even cycle of dark/light shades. */ int mrnaGrayIx = codonToGrayIx(mrnaBases, (grayIx > 26), NULL, - FALSE, TRUE); + FALSE, TRUE, NULL); if (color == cdsColor[CDS_START]) startColor = TRUE; color = colorAndCodonFromGrayIx(hvg, mrnaCodon, mrnaGrayIx, ixColor); if (startColor && sameString(mrnaCodon,"M")) color = cdsColor[CDS_START]; drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, mrnaCodon, zoomedToCodonLevel, winStart, maxPixels, isCoding, TRUE); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else if (drawOpt == baseColorDrawDiffBases) @@ -2227,55 +2241,98 @@ drawOpt == baseColorDrawDiffCodons || indelShowPolyA) { mrnaSeq = maybeGetSeqUpper(lf, qName, mrnaStart, mrnaEnd, tg->table, tg, doRc, &mrnaOffset); if (mrnaSeq == NULL) { drawOpt = baseColorDrawOff; } } } *retPsl = psl; *retMrnaSeq = mrnaSeq; *retMrnaOffset = mrnaOffset; return drawOpt; } +static void baseColorAddRulerCodonMapItem(struct hvGfx *hvg, struct simpleFeature *sf, + double scale, int xOff, int y, int height) +/* Add an image-map mouse-over to one ruler-codon box giving the amino acid's + * three-letter abbreviation and full name, e.g. "Ala (alanine)" (stop codons + * show "Ter (termination)"). The one-letter code is in sf->codonAa, set when + * the codon was translated in baseColorCodonsFromDna(). No-op for an + * error/partial codon or when not building an image-map (imageV2) image. */ +{ +if (!(theImgBox && curImgTrack)) + return; +char aa = sf->codonAa; +if (aa == '\0' || aa == 'X') + return; // error/partial codon: nothing meaningful to show +char title[128]; +if (aa == '*') + safecpy(title, sizeof(title), "Ter (termination)"); +else + { + char abbr[8]; + char *name = aaToName(aa); + aaToAbbr(aa, abbr, sizeof(abbr)); + if (name != NULL) + safef(title, sizeof(title), "%s - %s", abbr, name); + else + safef(title, sizeof(title), "%s", abbr); + } +int x1, x2; +if (scaledBoxToPixelCoords(sf->start, sf->end, scale, xOff, &x1, &x2)) + { + int w = x2 - x1; + if (w < 1) + w = 1; + x1 = hvGfxAdjXW(hvg, x1, w); // flip x for reverse-complement display + imgTrackAddMapItem(curImgTrack, TITLE_BUT_NO_LINK, title, + x1, y, x1 + w, y + height, NULL, NULL); + } +} + + void baseColorDrawRulerCodons(struct hvGfx *hvg, struct simpleFeature *sfList, double scale, int xOff, int y, int height, MgFont *font, int winStart, int maxPixels, bool zoomedToText) /* Draw amino acid translation of genomic sequence based on a list of codons. Used for browser ruler in full mode*/ { struct simpleFeature *sf; if (!cdsColorsMade) { makeCdsShades(hvg, cdsColor); cdsColorsMade = TRUE; } for (sf = sfList; sf != NULL; sf = sf->next) { char codon[4]; Color color = colorAndCodonFromGrayIx(hvg, codon, sf->grayIx, MG_GRAY); if (zoomedToText) drawScaledBoxWithText(hvg, sf->start, sf->end, scale, insideX, y, height, color, 1.0, font, codon, TRUE, winStart, maxPixels, TRUE, TRUE); else /* zoomed in just enough to see colored boxes */ drawScaledBox(hvg, sf->start, sf->end, scale, xOff, y, height, color); + + /* mouse-over the codon box with the amino acid's three-letter abbreviation + * and full name (sf->codonAa was set when the codon was translated) */ + baseColorAddRulerCodonMapItem(hvg, sf, scale, xOff, y, height); } } void baseColorSetCdsBounds(struct linkedFeatures *lf, struct psl *psl, struct track *tg) /* set CDS bounds in linked features for a PSL. Used when zoomed out too far * for codon or base coloring, but still want to render CDS bounds */ { struct genbankCds cds; getPslCds(psl, tg, lf, &cds); if (cds.start < cds.end) { struct genbankCds genomeCds = genbankCdsToGenome(&cds, psl); if (genomeCds.start < genomeCds.end)