ded41ea1968e30e2c248f65d1c9fec9793cb82fc max Thu Jun 18 11:31:32 2026 -0700 hgTracks: strand-direction arrows on the zoomed-in codon/CDS and UTR display When a coding transcript is zoomed in far enough to color its codons, the coding boxes carried no visible strand cue: only the introns showed the barbed "fishbone" arrows, and the strand was otherwise visible only on mouseover. This adds strand-direction chevrons to the codon and UTR display so the reading direction is visible at a glance. - At the amino-acid (codon-text) level, drawScaledBoxWithText() draws a chevron at each codon's left boundary (in the whitespace between the centered letters), in the box's contrasting color (white on the dark-blue codon shades), before the letter so the letter stays crisp. Points right on + strand, left on -; reverse-complement display is handled by hvGfxLine. The base-position ruler's codon display passes strand 0 and is unaffected. - When codons are colored but too small to label, baseColorDrawCdsArrows() distributes white chevrons across the coding part of each exon, on top of the boxes, at twice the standard barb spacing. - The UTRs (which never showed arrows) now get chevrons too, in the feature's contrasting color at a wider (3x) spacing as a cue that they are non-coding. The chevron is sized to the short UTR box, with a 1px floor so it still shows in squish mode. refs #37779 diff --git src/hg/hgTracks/cds.c src/hg/hgTracks/cds.c index 4a3dceb7a37..6d753b144a6 100644 --- src/hg/hgTracks/cds.c +++ src/hg/hgTracks/cds.c @@ -100,59 +100,100 @@ else if (btStart >= tEnd) { qEnd = lastBqEnd; foundEnd = TRUE; break; } } lastBqEnd = bqEnd; } *retQStart = qStart; *retQEnd = qEnd; return TRUE; } +static void drawCodonStrandArrow(struct hvGfx *hvg, int boundaryX, int midY, + int height, Color boxColor, int strand) +/* Draw a strand-direction chevron centered on the left boundary of a codon box + * (i.e. in the whitespace between the centered amino-acid letters of two + * adjacent codons). The chevron points in the strand direction (1 = right, + * -1 = left; reverse-complement display is handled by hvGfxLine). It is drawn + * in the box's contrasting color (white on the dark-blue codon shades, the same + * color as the codon letter) so it reads clearly, and the caller draws it before + * the amino-acid text so the letter stays crisp. */ +{ +if (strand == 0) + return; +int bh = (height - 2) / 2; // chevron half-height +if (bh > 3) + bh = 3; // keep it small +if (bh < 2) + return; // not enough vertical room + +// contrasting color: white on the dark-blue codon shades, matching the letter +Color aColor = hvGfxContrastingColor(hvg, boxColor); +if (strand > 0) + { + // ">" apex on the right, wings opening to the left + hvGfxLine(hvg, boundaryX + bh, midY, boundaryX - bh, midY - bh, aColor); + hvGfxLine(hvg, boundaryX + bh, midY, boundaryX - bh, midY + bh, aColor); + } +else + { + // "<" apex on the left, wings opening to the right + hvGfxLine(hvg, boundaryX - bh, midY, boundaryX + bh, midY - bh, aColor); + hvGfxLine(hvg, boundaryX - bh, midY, boundaryX + bh, midY + bh, aColor); + } +} + static void drawScaledBoxWithText(struct hvGfx *hvg, int chromStart, int chromEnd, double scale, int xOff, int y, int height, Color color, int score, MgFont *font, char *text, bool zoomed, - int winStart, int maxPixels, boolean isCoding, boolean justifyString) + int winStart, int maxPixels, boolean isCoding, boolean justifyString, + int strand) /* Draw a box scaled from chromosome to window coordinates with - a codon or set of 3 or less bases drawn in the box. */ + a codon or set of 3 or less bases drawn in the box. If strand is non-zero + and a whole codon is drawn, a faint strand-direction chevron is drawn in the + box before the amino-acid letter. */ { /*first draw the box itself*/ drawScaledBox(hvg, chromStart, chromEnd, scale, xOff, y, height, color); /*draw text in box if space, and align properly for codons or DNA*/ if (zoomed) { int i; Color textColor = hvGfxContrastingColor(hvg, color); int x1, x2, w; x1 = round((double)(chromStart-winStart)*scale) + xOff; x2 = round((double)(chromEnd-winStart)*scale) + xOff; if (x2 >= maxPixels) x2 = maxPixels - 1; w = x2-x1; if (w < 1) w = 1; if (chromEnd - chromStart == 3 && isCoding) { + /* faint strand arrow between this amino acid and the previous one, + * drawn before the letter so the letter stays legible */ + if (w >= 6) + drawCodonStrandArrow(hvg, x1, y + height/2, height, color, strand); if (justifyString) spreadBasesString(hvg, x1, y, w, height, textColor, font, text, strlen(text), TRUE); else hvGfxTextCentered(hvg, x1, y, w, height, textColor, font, text); } else if (chromEnd - chromStart < 3 && isCoding) { if (justifyString) spreadBasesString(hvg, x1, y, w, height, cdsColor[CDS_PARTIAL_CODON], font, text, strlen(text), TRUE); else hvGfxTextCentered(hvg, x1, y, w, height, cdsColor[CDS_PARTIAL_CODON], font, text); } else { int thisX,thisX2; @@ -1655,89 +1696,89 @@ mrnaBases[0] = '\0'; if (psl && isCoding) getMrnaBases(psl, mrnaSeq, mrnaOffset, mrnaS, s, e, (lf->orientation == -1), mrnaBases, &queryInsertion); if (queryInsertion && isCoding) color = cdsColor[CDS_QUERY_INSERTION]; dyStringAppendN(dyMrnaSeq, (char*)&mrnaSeq->dna[mrnaS - mrnaOffset], e-s); if (drawOpt == baseColorDrawItemBases) { 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); + zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE, lf->orientation); } 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, 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); + maxPixels, isCoding, TRUE, lf->orientation); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else if (drawOpt == baseColorDrawDiffBases) { char *diffStr = NULL; char *genoDna = getCachedDna(s, e); diffStr = needMem(sizeof(char) * (e - s + 1)); maskDiffString(diffStr, dyMrnaSeq->string, genoDna, ' ', dyMrnaSeq->stringSize); // fprintf(stderr, "drawOpt =- diffBases. %d %d %d %d\n", (int)strlen(genoDna), (int)strlen(dyMrnaSeq->string), (int)dyMrnaSeq->stringSize, e-s); if (cartUsualBooleanDb(cart, database, COMPLEMENT_BASES_VAR, FALSE)) complement(diffStr, strlen(diffStr)); drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, diffStr, - zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE); + zoomedToBaseLevel, winStart, maxPixels, isCoding, TRUE, lf->orientation); freeMem(diffStr); } else if (drawOpt == baseColorDrawDiffCodons) { if (e <= lf->tallEnd) { /* Color codons red wherever mrna differs from genomic; * keep the odd/even cycle of dark/light shades. */ colorAndCodonFromGrayIx(hvg, genomicCodon, grayIx, ixColor); int mrnaGrayIx = mrnaCodonToGrayIx(mrnaBases, genomicCodon[0], (grayIx > 26)); color = colorAndCodonFromGrayIx(hvg, mrnaCodon, mrnaGrayIx, ixColor); // Look up mrnaCodon again because if mrnaGrayIx is GRAYIX_SYN_PROT, // codon value is lost: safef(mrnaCodon, sizeof(mrnaCodon), "%c", baseColorLookupCodon(mrnaBases)); if (mrnaCodon[0] != genomicCodon[0]) { drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, mrnaCodon, zoomedToCodonLevel, - winStart, maxPixels, isCoding, TRUE); + winStart, maxPixels, isCoding, TRUE, lf->orientation); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, color); } else if (drawOpt != baseColorDrawCds) errAbort("Unknown drawOpt: %d
\n", drawOpt); dyStringFree(&dyMrnaSeq); } else { if (s < e) @@ -1767,37 +1808,37 @@ /* When we are zoomed out far enough so that multiple bases/codons share the * same pixel, we have to draw differences in a separate pass (baseColorOverdrawDiff) * so don't waste time drawing the differences here: */ boolean zoomedOutToPostProcessing = ((drawOpt == baseColorDrawDiffBases && !zoomedToBaseLevel) || (drawOpt == baseColorDrawDiffCodons && !zoomedToCdsColorLevel)); if (drawOpt == baseColorDrawGenomicCodons && (e-s <= 3)) { if (lf->highlightColor) { drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, lf->highlightColor); drawScaledBoxWithText(hvg, s, e, scale, xOff, y+1, heightPer-2, color, lf->score, font, codon, - zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex); + zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex, lf->orientation); } else { drawScaledBoxWithText(hvg, s, e, scale, xOff, y, heightPer, color, lf->score, font, codon, - zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex); + zoomedToCodonLevel, winStart, maxPixels, TRUE, !sf->codonIndex, lf->orientation); } } else if (qSeq != NULL && (psl != NULL || sf != NULL) && !zoomedOutToPostProcessing && drawOpt != baseColorDrawGenomicCodons && drawOpt != baseColorDrawOff) { if (lf->highlightColor) { drawScaledBox(hvg, s, e, scale, xOff, y, heightPer, lf->highlightColor); drawDiffTextBox(hvg, xOff+1, y+1, scale, heightPer-2, font, color, chromName, s, e, sf, psl, qSeq, qOffset, lf, grayIx, drawOpt, maxPixels, tg->colorShades, originalColor); } else @@ -1908,30 +1949,114 @@ enabled = FALSE; if (drawOpt == baseColorDrawDiffCodons && !zoomedToCdsColorLevel && lf->codons && enabled) { drawCdsDiffCodonsOnly(tg, lf, hvg, xOff, y, scale, heightPer, qSeq, qOffset, psl, winStart); } if (drawOpt == baseColorDrawDiffBases && !zoomedToBaseLevel && enabled) { drawCdsDiffBaseTickmarksOnly(tg, lf, hvg, xOff, y, scale, heightPer, qSeq, qOffset, psl, winStart); } } +static void drawStrandBarbsInRange(struct hvGfx *hvg, int s, int e, double scale, + int xOff, int winStart, int midY, int barbHeight, int barbSpacing, + int orientation, Color color) +/* Clip base range [s,e] to the window and draw evenly-spaced strand chevrons + * (barbs only, no connecting line) across it. */ +{ +if (s < winStart) s = winStart; +if (e > winEnd) e = winEnd; +if (e <= s) + return; +int x1 = round((double)(s-winStart)*scale) + xOff; +int x2 = round((double)(e-winStart)*scale) + xOff; +int w = x2 - x1; +if (w < barbHeight*2) + return; // too narrow to show an arrow cleanly +clippedBarbs(hvg, x1, midY, w, barbHeight, barbSpacing, orientation, color, FALSE); +} + +void baseColorDrawCdsArrows(struct track *tg, struct linkedFeatures *lf, + struct hvGfx *hvg, int xOff, int y, double scale, + int heightPer, int winStart, enum baseColorDrawOpt drawOpt, + Color color) +/* When zoomed in far enough to color the codons, distribute strand-direction + * chevrons across each exon on top of the boxes (the per-codon letter arrows in + * drawScaledBoxWithText only appear once the codons are big enough to label). + * The coding part gets white chevrons at the standard barb spacing, but only + * when the codons are too small to label (otherwise the letter arrows cover it). + * The UTR parts get the feature's contrasting color at a wider spacing, as a + * visual hint that they are non-coding. No-op below the codon-color zoom level, + * when coding coloring is off, or when the strand is unknown. */ +{ +if (drawOpt <= baseColorDrawOff) + return; +if (!zoomedToCdsColorLevel) + return; +if (lf->orientation == 0) + return; + +if (!cdsColorsMade) + { + makeCdsShades(hvg, cdsColor); + cdsColorsMade = TRUE; + } + +int midY = y + (heightPer>>1); +int orientation = lf->orientation; + +/* white reads clearly on top of the dark-blue codon shades (the same high + * contrast the codon-letter text uses); only draw it when the codons are too + * small to show their letters (which carry their own arrows) */ +boolean drawCds = !zoomedToCodonLevel; +Color cdsColor1 = hvGfxFindColorIx(hvg, 0xff, 0xff, 0xff); +int cdsBh = tl.barbHeight; +int cdsSpacing = tl.barbSpacing*2; + +/* UTR boxes are drawn shorter and in the feature color, so size the chevron to + * the short box (but at least 1px, so it still shows in squish mode where the + * box is only a few pixels tall) and use the feature's contrasting color; space + * them more widely than the coding chevrons as a cue that UTRs are non-coding */ +int shortOff = heightPer/4; +int shortHeight = heightPer - 2*shortOff; +int utrBh = shortHeight/2; +if (utrBh > tl.barbHeight) + utrBh = tl.barbHeight; +if (utrBh < 1) + utrBh = 1; +Color utrColor = hvGfxContrastingColor(hvg, color); +int utrSpacing = tl.barbSpacing*3; + +struct simpleFeature *sf; +for (sf = lf->components; sf != NULL; sf = sf->next) + { + if (drawCds) + drawStrandBarbsInRange(hvg, max(sf->start, lf->tallStart), min(sf->end, lf->tallEnd), + scale, xOff, winStart, midY, cdsBh, cdsSpacing, orientation, cdsColor1); + /* 5' and 3' UTR portions of this exon (the parts outside [tallStart,tallEnd]) */ + drawStrandBarbsInRange(hvg, sf->start, min(sf->end, lf->tallStart), + scale, xOff, winStart, midY, utrBh, utrSpacing, orientation, utrColor); + drawStrandBarbsInRange(hvg, max(sf->start, lf->tallEnd), sf->end, + scale, xOff, winStart, midY, utrBh, utrSpacing, orientation, utrColor); + } +} + + void baseColorOverdrawQInsert(struct track *tg, struct linkedFeatures *lf, struct hvGfx *hvg, int xOff, int y, double scale, int heightPer, struct dnaSeq *qSeq, int qOffset, struct psl *psl, MgFont *font, int winStart, enum baseColorDrawOpt drawOpt, boolean indelShowQInsert, boolean indelShowPolyA) /* If applicable, draw 1-pixel wide orange lines for query insertions in the * middle of the query, 1-pixel wide blue lines for query insertions at the * end of the query, and 1-pixel wide green (instead of blue) when a query * insertion at the end is a valid poly-A tail. */ { assert(psl); int i; int s; int lastBlk = psl->blockCount - 1; @@ -2301,31 +2426,31 @@ 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); + winStart, maxPixels, TRUE, TRUE, 0); 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 */